使foo(derived_object)调用foo(Base const&)而不是模板函数?

时间:2016-03-25 16:19:11

标签: c++ overload-resolution

鉴于此代码:

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.
}

...我希望调用调用为Base定义的重载,而不必为Derived定义重载或模板特化。

目标是能够将foo应用于所有类型的对象,而不仅仅是Base(和Derived)个对象,以及大多数对象的通用实现。< / p>

如果答案在这种情况下确切解释了重载决策是如何工作的,而不是如何定义Derived重载,那也很好。

在出现此问题的代码中,foo上面的函数模板n_items定义如下:

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 ); }

目的是这对于没有定义自己的n_items重载的类型应该是一个全能的。

对于基类和派生类,基类定义自定义n_items应该足够了;必须为每个派生类定义它是非常多余的。

5 个答案:

答案 0 :(得分:5)

重载解析如何工作

首先,我们进行名称查找和模板类型推导。对于foo(d),这给了我们两个选项:

  1. foo(Derived const&)[C = Derived]
  2. foo(Base const&)
  3. 这些是我们可行的候选人。

    然后我们确定哪一个重载是最佳的可行候选者。首先通过查看转换序列来完成。在这种情况下,(1)是完全匹配,而(2)涉及具有转换等级的派生到基础转换(参见this table)。由于排名更差,候选人(1)是首选,因此被认为是最佳的可行候选人。

    如何做你真正想要的事情

    最简单的方法是简单地为Derived添加第三个重载:

    1. foo(Derived const&)
    2. 这里,(1)和(3)在转换序列方面都是等价的。但是,不是模板的函数比作为模板的函数更受欢迎(将其视为选择最具体的选项),因此将选择(3)。

      但你不想这样做。因此,选项可以是:make(1)不适用于Derived或make(2)也适用于Derived

      禁用常规模板

      我们可以使用SFINAE简单地排除从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)
      

      现在,(1)不再是比赛的可行候选人,所以(2)是非常优先的。

      重做重载,以便首选

      Xeo's book中取出一个小费,我们可以重新调整过载:

      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>{}); }
      

      现在我们可以选择从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)
      

      这改变了重载决策如何工作的机制,值得进入不同的案例。

      致电foo(d)表示我们正在致电foo_impl(d, choice<0> )并为我们提供两位可行的候选人:

      1. foo_impl(Derived const&, choice<0> )[T = Derived]
      2. foo_impl(Derived const&, otherwise )[T = Derived]
      3. 这里,第一个参数是相同的,但对于第二个参数,第一个重载是精确匹配,而第二个参数需要通过可变参数进行转换。作为结果,otherwise始终是单词选择,因此首选过载是首选。正是我们想要的。

        另一方面,调用foo(not_a_base)只会给我们一个可行的候选人:

        1. foo_impl(NotABase const&, otherwise )[T = NotABase]
        2. 另一个因扣除失败而被删除。所以这是最好的可行候选人,而且我们也能得到我们想要的东西。

          对于您的具体问题,我写了类似的内容:

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

          使用:

          // 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);
          }
          

答案 1 :(得分:3)

将所有非模板函数放在某个命名空间中:

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

然后定义函数模板:

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。当且仅当非模板重载都不匹配(或不明确)时,这将调用一般重载。

答案 2 :(得分:3)

主要的技术问题是,为foo参数实例化的通用函数模板Derived(比如说​​)是比Base参数的重载更好的匹配。

强力解决方案(冗余)。

一个简单的可能解决方案是要求从Base派生的每个类的所有相关函数的重载。

然而,这打破了 DRY 规则,不要重复自己

代码中不必要的冗余会导致维护问题。

SFINAE的命名空间。

另一个可能的解决方案suggested by Columbo是“将所有非模板函数放在某个[特定单个]命名空间中”,这允许通用函数模板使用SFINAE来检测是否存在相关的重载。 / p>

此解决方案避免了冗余,并且我记得它是用于boost::intrusive_ptr的解决方案,客户端代码必须专门化某些特定命名空间中的功能。

但是,由于它对客户端代码施加了限制,因此对我来说感觉并不完美。

直接重载解析的额外参数。

第三种可能的解决方案suggested by Barry,是让基类实现有一个关联的重载,或者只是一个不同命名的函数,它有一个额外的参数用于指导重载决策,并且被调用通过一般功能模板。

这在技术上是一个解决方案,但IMO并不干净:客户端代码调用与必须提供的重载不匹配。

同样存在潜在的维护问题。

从简单的转换过载调用函数模板。

Johannes Schaub suggested第四种可能的解决方案,它允许干净,简单的客户端代码,即让普通的重载调用函数模板,但是那个通用的实现重载具有正式的参数类型

  1. 引入了用户定义的转换,这使得它比直接的每类重载更加匹配,并且

  2. 有一个模板化的构造函数,用于获取实际的参数类型。

  3. 约翰&#39;原始巧妙的评论中的想法假设一个固定的函数结果类型和一个参数。将简短的概念描述推广到任意结果类型对我来说并不是完全无关紧要的,因为结果类型可以取决于实际的参数,并且人们倾向于首先尝试自动化这样的事情。同样,推广到任意数量的参数,第一个参数作为this参数,对我来说并非100%微不足道。毫无疑问,约翰内斯在这方面没有任何问题,并且可能以比我能做到的更整洁的方式。无论如何,我的代码:

    #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;
    }
    

    我没有尝试过支持rvalue引用参数。

    输出:
    101 102
    201 202
    

答案 3 :(得分:0)

变体Johannes Schaub's suggestion,似乎产生最干净的用法,在开始时写为示例代码:

//-------------------------------------- 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
}

答案 4 :(得分:0)

我不确定我是否遇到了这个问题,但是你不能简单地使用SFINAE和is_base_of特征吗? 使用它们时,如果对从Base派生的类进行函数分辨并且最佳匹配是非模板,则会自动排除捕获所有函数。
而且,这样的解决方案在我看来比其他所有解决方案都简单得多......这就是为什么我很确定我没有解决问题的原因! : - )

无论如何,它遵循一个工作的例子:

#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;
}

如果我误解了这个问题,请告诉我,在这种情况下,我放弃了答案。