Make foo(derived_object) call foo(Base const&) instead of template function?

ぐ巨炮叔叔 提交于 2019-11-30 18:13:14

问题


Given this code:

template< class C >
void foo( C const& o ) { o.nosuch(); }

struct Base {};
void foo( Base const& ) {}

struct Derived: Base {};

auto main() -> int
{
    Derived d;
    foo( d );       // !Invokes template.
}

… I want the call to invoke the overload defined for Base, without having to define an overload or template specialization for Derived.

The goal is to be able to apply foo to all kinds of objects, not just Base (and Derived) objects, with a generic implementation for most kinds of objects.

It would also be nice if an answer explained exactly how overload resolution works in this case, versus how it works with a Derived overload defined.


In the code where this problem manifested, foo above is a function template n_items defined as follows:

template< class Type >
auto n_items( Type const& o )
    -> size_t
{ return o.size(); }

template< size_t n >
auto n_items( std::bitset<n> const& o )
    -> size_t
{ return o.count(); }       // Corresponds to std::set<int>::size()

template< class Type, size_t n >
constexpr
auto n_items( Raw_array_of_<n, Type> const& a )
    -> size_t
{ return static_size( a ); }

And the intent is that this should be a catch-all for types that don't define their own n_items overloads.

And for a base and derived class, it should be enough that the base class defines a custom n_items; it would be very redundant to have to define it for every derived class.


回答1:


How overload resolution works

First we do name lookup and template type deduction. For foo(d), this gives us two options:

  1. foo(Derived const&), with [C = Derived]
  2. foo(Base const&)

These are our viable candidates.

Then we determine which one of the overloads is the best viable candidate. This is done first by looking at conversion sequences. In this case, (1) is an Exact Match whereas (2) involves a derived-to-base conversion which has Conversion rank (see this table). Since that ranks worse, candidate (1) is preferred and is thus deemed the best viable candidate.

How to do what you really want

The simplest way is to simply add a third overload for Derived:

  1. foo(Derived const&)

Here, both (1) and (3) would be equivalent in terms of conversion sequence. But functions that aren't templates are preferred to functions that are templates (think of it as picking the most specific option), so (3) would be selected.

But you don't want to do that. So the options are either: make (1) not work for Derived or make (2) work for Derived too in a way that's preferred.

Disable the general template

We can use SFINAE to simply exclude anything derived from Base:

template <class T, class = std::enable_if_t<!std::is_convertible<T*, Base*>::value>
void foo(T const& ); // new (1)

void foo(Base const& ); // same (2)

Now, (1) is no longer a viable candidate for the match, so (2) is trivally preferred.

Redo the overloads so that the Base is preferred

Taking a tip out of Xeo's book, we can restructure the overloads thusly:

template <int I> struct choice : choice<I + 1> { };
template <> struct choice<10> { };
struct otherwise { otherwise(...) {} };

template <class T> void foo(T const& val) { foo_impl(val, choice<0>{}); }

And now we can prefer those types derived from Base:

template <class T, class = std::enable_if_t<std::is_convertible<T*, Base*>::value>>
void foo_impl(T const& val, choice<0> ); // new (2)

template <class T>
void foo_impl(T const& val, otherwise ); // new (1)

This changes the mechanics of how overload resolution works and is worth going through into the separate cases.

Calling foo(d) means we're calling foo_impl(d, choice<0> ) and gives us two viable candidates:

  1. foo_impl(Derived const&, choice<0> ), with [T = Derived]
  2. foo_impl(Derived const&, otherwise ), with [T = Derived]

Here, the first argument is identical, but for the second argument, the first overload is an Exact Match while the second argument requires a Conversion via a variadic argument. otherwise will always be the word choice as a result, so the first overload is preferred. Exactly what we want.

Calling foo(not_a_base), on the other hand, would only give us one viable candidate:

  1. foo_impl(NotABase const&, otherwise ), with [T = NotABase]

The other one was removed due to the deduction failure. So this is trivially the best viable candidate, and again we get exactly what we want.


For your specific question, I'd write something like:

template <class T>
size_t n_items(T const& cont) { return n_items(cont, choice<0>{}); }

with:

// has count?
template <class T>
auto n_items(T const& cont, choice<0> ) -> decltype(cont.count()) {
    return cont.count();
}

// else, has size?
template <class T>
auto n_items(T const& cont, choice<1> ) -> decltype(cont.size()) {
    return cont.size();
}

// else, use static_size
template <class T>
size_t n_items(T const& cont, otherwise )
    return static_size(cont);
}



回答2:


Put all the non-template functions in some namespace:

namespace Foo {
    void foo( Base const& ) {}
}

Then define the function template thusly:

template <typename C>
auto foo_aux(C const& c, int) -> decltype(Foo::foo(c)) {
    return Foo::foo(c);}
template <typename C>
void foo_aux(C const& c, ...) {
    c.nosuch();}

template <typename C>
void foo(C const& c) {foo_aux(c, 0);}

Demo. This will call the general overload if and only if none of the non-template overloads match (or are ambiguous).




回答3:


The main technical problem is that a general function template foo (say), instantiated for Derived argument, is a better match than the overload for Base argument.


Brute force solution (redundancy).

One simple possible solution is to require overloads of all the relevant functions for every class derived from a Base.

However, this breaks the DRY rule, Don't Repeat Yourself.

Needless redundancy in code leads to maintenance problems.


Namespace for SFINAE.

Another possible solution, suggested by Columbo, is to “put all the non-template functions in some [specific single] namespace”, which allows a general function template to use SFINAE to detect if a relevant overload exists.

This solution avoids the redundancy, and as I recall it is the kind of solution used for boost::intrusive_ptr, that client code must specialize the functionality in some specific namespace.

However, since it imposes a restriction on client code it feels not perfect to me.


Extra argument to direct overload resolution.

A third possible solution, suggested by Barry, is to let the base class implementation have an associated overload, or just a differently named function, that has an extra argument that serves to direct the overload resolution, and that is called by by a general function template.

This is technically a solution but IMO it's not clean: the client code invocations don't match the overloads that must be provided.

And so again there is a potential maintenance problem.


Invoking the function template from a simple converting overload.

Johannes Schaub suggested a fourth possible solution, which allows clean, simple client code, namely to let an ordinary overload call the function template, but where that general implementation overload has a formal argument type that

  1. introduces a user defined conversion, which makes this a worse match than direct per-class overloads, and

  2. has a templated constructor that picks up the actual argument type.

Johannes' original ingenious in-comment idea assumed a fixed function result type and exactly one argument. Generalizing that short idea description to arbitrary result type was not entirely trivial for me because that result type can depend on the actual argument, and one tends to first of all try to automate things like that. Likewise, generalizing to arbitrary number of arguments, with the first one acting as a this argument, was not 100% trivial for me. Johannes would no doubt have no problems doing this, and probably in a more neat way than I could do it. Anyway, my code:

#include <functional>
#include <utility>


//-------------------------------------- General machinery:

template< class Impl_func, class Result, class... Args >
class Invoker_of_
{
private:
    std::function< auto(Args...) -> Result > f_;

public:
    auto operator()( Args... args ) const -> Result { return f_( args... ); }

    template< class Type >
    Invoker_of_( Type& o )
        : f_(
            [&]( Args... args )
                -> Result
            { return Impl_func{}.operator()( o, args... ); }
            )
    {}
};


//-------------------------------------- General template 1 (foo):

struct Foo_impl
{
    template< class Type >
    auto operator()( Type& o )
        -> int
    { return o.foomethod(); }
};

auto foo( Invoker_of_<Foo_impl, int> const invoker )
    -> int
{ return invoker(); }


//-------------------------------------- General template 2 (bar):

struct Bar_impl
{
    template< class Type >
    auto operator()( Type& o, int const arg1 )
        -> int
    { return o.barmethod( arg1 ); }
};

auto bar( Invoker_of_<Bar_impl, int, int> const invoker, int const arg1 )
    -> int
{ return invoker( arg1 ); }


//--------------------------------------- Usage examples:

struct Base {};
auto foo( Base const& ) -> int { return 101;}
auto bar( Base const&, int x ) -> int { return x + 2; }

struct Derived: Base {};

struct Other
{
    auto foomethod() -> int { return 201; }
    auto barmethod( int const x ) -> int { return x + 2; }
};


//--------------------------------------- Test driver:

#include <iostream>
using namespace std;
auto main() -> int
{
    Derived d;
    int const r1 = foo( d );            // OK, invokes non-template overload.
    int const r2 = bar( d, 100 );       // OK, invokes non-template overload.
    cout << r1 << " " << r2 << endl;

    Other o;
    int const r3 = foo( o );            // OK, invokes the general template.
    int const r4 = bar( o, 200 );       // OK, invokes the general template.
    cout << r3 << " " << r4 << endl;
}

I have not attempted to support rvalue reference arguments.

Output:
101 102
201 202



回答4:


A variation Johannes Schaub's suggestion, which appears to yield the most clean usage, written up as code for the example at the start:

//-------------------------------------- Machinery:

template< class Type >
auto foo_impl( Type& o ) -> int { return o.method(); }

struct Invoker_of_foo_impl
{
    int result;

    template< class Type >
    Invoker_of_foo_impl( Type& o ): result( foo_impl( o ) ) {}
};

auto foo( Invoker_of_foo_impl const invoker ) -> int { return invoker.result; }

//--------------------------------------- Usage:

struct Base {};
auto foo( Base const& ) -> int { return 6*7;}

struct Derived: Base {};

struct Other { auto method() -> int { return 0b101010; } };

auto main() -> int
{
    Derived d;
    foo( d );       // OK, invokes non-template.

    Other o;
    foo( o );       // OK, invokes template
}



回答5:


I'm not sure I got the problem, but can't you use simply SFINAE and the is_base_of trait?
Using them, catch all functions are automatically excluded when the function resolution takes place for a class that is derived from Base and the best match is the non-template one.
Moreover, such a solution looks to me as far simpler than all the other... That's why I'm pretty sure that I didn't get the problem!! :-)

Anyway, it follows a working example:

#include<type_traits>
#include<iostream>

struct Base {};
auto foo( Base const& ) -> int {return 101;}
auto bar( Base const&, int x ) -> int {return x + 2; }

template<class T, typename = typename std::enable_if<not std::is_base_of<Base, T>::value>::type>
auto foo(T & t) -> int {
    return t.foomethod();
}

template<class T, typename = typename std::enable_if<not std::is_base_of<Base, T>::value>::type>
auto bar(T & t, int i) -> int {
    return t.barmethod(i);
}

struct Derived: Base {};

struct Other
{
    auto foomethod() -> int { return 201; }
    auto barmethod( int const x ) -> int { return x + 2; }
};

#include <iostream>
using namespace std;
auto main() -> int
{
    Derived d;
    int const r1 = foo( d );            // Invokes the Base arg overload.
    int const r2 = bar( d, 100 );       // Invokes the Base arg overload.
    cout << r1 << " " << r2 << endl;

    Other o;
    int const r3 = foo( o );            // Invokes the general implementation.
    int const r4 = bar( o, 200 );       // Invokes the general implementation.
    cout << r3 << " " << r4 << endl;
}

Let me know if I've misunderstood the problem, in that case I'm dropping the answer.



来源:https://stackoverflow.com/questions/36223766/make-fooderived-object-call-foobase-const-instead-of-template-function

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!