【问题标题】:Override template function with abstract class用抽象类覆盖模板函数
【发布时间】:2019-08-05 21:15:39
【问题描述】:

我创建了一个模板函数,定义如下。

template<class T>
void func(T t){ /* do stuff */ }

如果 T 从我创建的抽象类继承,我想重载这个模板。

class A {
public:
    virtual void doStuff() = 0;
};
class B : public A {
    virtual void doStuff(){ /* do stuff */ }
}

我尝试过使用模板特化(如下),但它仍然使用原始定义。

template<>
void func(A& a){ /* do different stuff */ } // Not called by func(B())

我也尝试过重载它,虽然这适用于整数,但不适用于我的基类。

func(int i){ /* do stuff with i */ } // Called by func(3)
func(A& a){ /* do different stuff */ } // Not called by func(B())

我猜这与 C++ 不希望将我的 B 实例隐式转换为 A 并引用它有关,但我无法找到任何解释如何解决此问题的信息。由于A有一个纯虚函数,我不能只定义func(A a)。任何帮助将不胜感激。

这是一个可以重现我正在经历的行为的示例。

#include <iostream>

template<class T>
void func(T t){
    std::cout << "Template function called!" << std::endl;
}

class A {
public:
    virtual void doStuff() = 0;
};
class B : public A{
public:
    virtual void doStuff(){};
};

template<>
void func(const A& a){
    std::cout << "Specialized template called!" << std::endl;
}

void func(const A& a){
    std::cout << "Overload called!" << std::endl;
}

int main(){
    B b{};
    func(b);

    return 0;
}

【问题讨论】:

  • 这个问题很可能是因为在调用点之后声明了特化/重载。请发帖minimal reproducible example
  • 我想你可以在这里找到问题的答案:stackoverflow.com/a/28406090/246759
  • @mortenvp 这确实解释了原因,但没有解释解决方案
  • @1201ProgramAlarm 这仍然不够,因为func&lt;B&gt;(B) 仍然比重载func(const A&amp;) 更适合

标签: c++ templates polymorphism overloading


【解决方案1】:

如果您可以访问 C++17,那么您可以创建一个公共重载,该重载将转发到正确的函数:

namespace detail {
  template<class T>
  void func(T t) {
    std::cout << "Template function called!" << std::endl;
  }

  void func(A& a){
    std::cout << "Overload called!" << std::endl;
  }
}

template<class T>
void func(T& t) {
  if constexpr (std::is_base_of_v<A, T>) {
    detail::func(static_cast<A&>(t));
  } else {
    detail::func(t);
  }
}

int main() {
  B b;
  func(b);
}

否则你可以使用标签调度或 SFINAE:

标签调度:

template<class T>
void func(T t, std::false_type) {
  std::cout << "Template function called!" << std::endl;
}

void func(A& a, std::true_type) {
  std::cout << "Other function called!" << std::endl;
}

template<class T>
void func(T& t) {
  func(t, std::is_base_of<A, T>{});
}

SFINAE:

template<class T,
         std::enable_if_t<std::is_base_of_v<A, T>>* = nullptr>
void func(T t) {
  std::cout << "Template function called!" << std::endl;
}

template<class T,
         std::enable_if_t<!std::is_base_of_v<A, T>>* = nullptr>
void func(T& t) {
  std::cout << "Other function called!" << std::endl;
}

【讨论】:

    【解决方案2】:

    C++20解决方案

    You can run the code here.

    使用 C++20 中的概念,我们可以编写一个 inherits_from 概念,并使用它。概念允许我们约束模板,使其仅适用于表达式为真的情况。

    这个概念是这样的:

    #include <type_traits>
    
    template<class Derived, class Base>
    concept derived_from = std::is_base_of_v<Base, Derived>;
    

    然后,我们可以编写泛型模板和约束模板:

    struct MyBase{};
    struct MyDerived : MyBase{}; 
    
    // This is the generic template; using auto here is valid in C++20
    void do_thing(auto const& thing) {
        std::cout << "Doing thing on regular type\n";
    }
    
    //This is the template that acts on classes derived from MyBase
    void do_thing(derived_from<MyBase> const& x) {
        std::cout << "Doing thing on MyBase\n";
    }
    

    因为第二个函数将T 声明为遵循inherits_from 的概念,所以它更加专业化,因此对于实际继承自MyBase 的类型,它将在通用模板上被选中:

    int main() {
        do_thing(10);           // Prints "Doing thing on regular type"
        do_thing(MyBase());     // Prints "Doing thing on MyBase"
        do_thing(MyDerived());  // Prints "Doing thing on MyBase"
    }
    

    C++17 解决方案

    You can run the code here.

    我们可以使用 SFINAE 模拟概念的行为,尽管这需要修改通用模板,以便在 T 扩展 MyBase 时忽略它。使用 SFINAE 的关键是在条件为假时触发替换失败,从而导致忽略该重载。

    为了触发替换失败,请在模板参数列表的末尾添加一个默认模板参数。在我们的例子中,它看起来像这样:

    template<
        class T,
        // This defaulted argument triggers the substitution failure
        class = std::enable_if_v</* condition */>> 
    

    在我们的代码中, - 如果T 扩展MyBase,通用重载将被禁用 - 如果T 扩展MyBase

    ,约束重载将被禁用

    看起来像这样:

    struct MyBase {};
    struct MyDerived : MyBase {};
    
    template<
        class T,
        class = std::enable_if_t<!std::is_base_of_v<MyBase, T>>>
    void do_thing(T const&) {
        std::cout << "Doing thing on regular type\n";
    }
    
    // This overload is *disabled* if T doesn't inherit from MyBase
    template<
        class T,
        // We have to have an additional defaulted template argument to distinguish between the overloads
        class = void, 
        class = std::enable_if_t<std::is_base_of_v<MyBase, T>>>
    void do_thing(T const& x) {
        std::cout << "Doing thing on MyBase\n";
    }
    

    尽管声明很奇怪,我们仍然可以使用do_thing,就好像它是一个常规函数:

    int main() {
        do_thing(10);
        do_thing(MyBase());
        do_thing(MyDerived()); 
    }
    

    向后移植到 C++11

    You can run the code here.

    我们只需要做一些小的改动就可以将内容向后移植到 C++11。基本上,

    • is_base_of_v&lt;MyBase, T&gt; 必须替换为 is_base_of&lt;MyBase, T&gt;::value,并且
    • enable_if_t&lt;/* condition */&gt; 必须替换为 typename enable_if&lt;/* condition */&gt;::type

    【讨论】:

    • 谢谢!有没有办法在 C++17 或 C++14 中实现类似的东西?
    • 有,但更丑。我更新了答案以解释如何做到这一点。如果您有任何其他问题,请告诉我!
    【解决方案3】:
    B b;
    func((A&)b);
    

    我不认为你可以在这里传递一个临时的。您不能将 B() 转换为 'A&',因为它是一个右值,并且将其转换为 const A& 会使模板版本更好地匹配。

    使用 c++11 的另一个选项(在其他答案之上) - 如果 T 是 B 的子类型,则显式删除模板版本:

    template<class T>
    typename std::enable_if<!std::is_base_of<A, T>::value>::type
    func(T& t){
        std::cout << "Template function called!" << std::endl;
    }
    
    void func(const A& a){
        std::cout << "Overload called!" << std::endl;
    }
    
    int main(){
      func(B());
    }
    

    【讨论】:

    • 这样硬铸造会导致任何问题吗?例如,如果 B 具有从其他类继承的其他虚函数,那么 v 表是否仍能正确匹配 A 的表?
    • 你可以测试它,但对我来说它完全等同于 B b; A&amp; a = b; func(a); 所以没有 vtable 问题。
    猜你喜欢
    • 2015-02-25
    • 2015-01-05
    • 1970-01-01
    • 2014-08-16
    • 2021-11-08
    • 2023-04-04
    • 1970-01-01
    • 2014-03-08
    • 1970-01-01
    相关资源
    最近更新 更多