首页 > 解决方案 > 模板化函数或带有指向基类的指针的函数

问题描述

当我想对几种不同类型的输入类使用一个函数(这里称为do_some_work()/ do_some_templated_work())并且我不想多次重写该函数时,据我所知,我可以模板该函数,或者从一个基类,并使用该基类的指针。我为这两种情况编写了一个简短的测试程序:

#include <iostream>
#include <string>


class BaseClass {
public:
    BaseClass(){
        class_name = std::string("Base class");
    }

    virtual ~BaseClass(){};

    virtual double work_func(const double a, const double b) {
        (void) a;
        (void) b;
        return 0;
    };
    virtual void print_class_name(void) {};
private:
    std::string class_name;
};

class DerivedClassA : public BaseClass{
public:
    DerivedClassA(){
        class_name = std::string("Class A");
    }

    ~DerivedClassA() override {

    }

    double work_func(const double a, const double b) override{
        return a + b;
    }

    void print_class_name(void) override{
        std::cout << class_name << '\n';
    }

private:
    std::string class_name;
};

class DerivedClassB : public BaseClass{
public:
    DerivedClassB(){
        class_name = std::string("Class B");
    }

    ~DerivedClassB() override {

    }

    double work_func(const double a, const double b) override{
        return a - b;
    }

    void print_class_name(void) override{
        std::cout << class_name << '\n';
    }
private:
    std::string class_name;
};

void do_some_work(BaseClass &test_class){
    test_class.print_class_name();
    std::cout << test_class.work_func(5, 6) << '\n';
}

template <class T>
void do_some_templated_work(T &test_class) {
    test_class.print_class_name();
    std::cout << test_class.work_func(5, 6) << '\n';
}

int main()
{
    std::cout << "Hello World!" << std::endl;
    DerivedClassA AClass;
    DerivedClassB BClass;
    do_some_work(AClass);
    do_some_work(BClass);
    do_some_templated_work(AClass);
    do_some_templated_work(BClass);
    return 0;
}

在查看 ASM 代码时,我没有看到两者的直接优势(不过,这可能与编译开关有关)。因此,这里有什么我没有考虑的事情吗?在比较两者时,这两种方法是否各有优缺点?或者是否有第三种方法可以用于相同目的?

标签: c++classtemplates

解决方案


一般来说,第一个选项涉及虚拟分派(即在运行时跳转到正确的函数),而第二个选项已经知道要在编译时调用的正确函数。后者通常具有较少的开销并为编译器开辟了更多优化机会,但可能存在缺点(代码大小、指令缓存等)。性能将始终取决于细节,因此,如果您关心它,请配置文件。

开箱即用的继承无法做到的事情是,例如从其中返回不同类型的值work_func——这就是模板大放异彩的地方。

另一方面,继承(尤其是在遵循 Liskov 替换原则时)可以使接口的约定/期望更加清晰(void do_some_templated_work(T &test_class)不会告诉您T需要实现例如print_class_name)。


推荐阅读