执行特定函数取决于类型

Executing a specific function depend on type

本文关键字:取决于 类型 函数 执行      更新时间:2023-10-16

我想使一个可以处理不同类型事物的函数合二为一。

我知道重载是一个很好的解决方案。就像,

class C1 {...};
class C2 {...};    
void handle(C1& c1){...}
void handle(C2& c2){...}

但是重载方式中有很多重复的代码,因为这 2 个初始化是相同的。这就是为什么我想把它们包在一起。

我有一些想法来实现我的目的。 例如

class C1 {...};
class C2 {...};
void handle_C1(C1 &c1);
void handle_C2(C2 &c2);
template<typename T>
void handle(T &content){
// Initialization was done here
if (std::is_same(C1, T))
handle_C1(content);
if (std::is_same(C2, T))
handle_C2(content);
}

发现编译错误错误,handle_C2参数不匹配,因为我调用时handle_C2参数类型为 C2

C1 c1;
handle_C1<C1>(c1);

根据SFINAE在C++年的说法,我希望编译会忽略这种替换失败,但事实并非如此。

有人可以给我建议吗?

最好的解决方案是重载吗? 如果这是真的,我怎样才能减少重复的代码。

在我看来,你想多了。只需定义重载而无需初始化代码,并定义一个利用重载解析的handle函数。

class C1 {...};
class C2 {...};    
void handle_impl(C1& c1){...} // Remove initialization from your overloads
void handle_impl(C2& c2){...}
template<typename T>
void handle(T &content)
{
// Initialization is done here
// Let the compiler resolve overloads for you
handle_impl(content);
}

当 handle() 使用 T 实例化时,函数的整个主体被实例化。"如果"和它们的身体都被编译了。 虽然你和我知道 is_same() 是一个编译时常量,你可能希望编译器忽略不可能的情况,但它的使用方式是作为运行时值,编译器必须处理 ifs 和语义检查"好像"它们都可能被调用(即使优化器可以消除一个, 这是在编译器进行的有效性测试之后。 因此,您最终会得到调用 handle_C1 和 handle_C2 的代码,两者都传递相同的类型,并且其中一个肯定无效且无法编译。

如果你可以使用 c++17,一个新功能直接解决了这个问题,称为"constexpr if",如果 constexpr 为真,它会使 if 的主体得到处理(除了有效的语句和语法):

template<typename T>
void handle(T &content){
// Initialization was done here
if constexpr (std::is_same(C1, T))
handle_C1(content);
else if constexpr (std::is_same(C2, T))
handle_C2(content);
}

如果你没有 c++17 编译器(或者即使你有!),你应该考虑回到你的原始设计,简单地从每个函数中分解出初始化,并使其成为一个通用的帮助函数。

我认为在您的情况下,重载就是您所需要的。只需为您编写另一个特殊初始化方法来处理每个类,并在模板化版本(在线)中执行常规操作:

void handleSpecial(C1& c1) {
std::cout << "Handling C1n";
}
void handleSpecial(C2& c2) {
std::cout << "Handling C2n";
}
template <typename T>
void handle(T& content) {
std::cout << "Doing generell stuffn";
handleSpecial(content);
}

SFINAE 仅在选择要调用的函数之前工作,在这种情况下,这不会发生,因为可以使用任何类型的函数调用。然后将生成整个主体,并且您会收到编译器错误,因为没有用于C2的函数handle_C1,反之亦然。

完成您的步骤:

如果你有多个类,这些类在许多事情上处理相同,但具有单独的字符,那么这可能需要一个基类。 例如,你可以有这样的东西:

class CBase { ... common features of C1, C2, C3, ... 
public:
~CBase() {};
virtual void handle() = 0;
};
class C1 : public CBase { ... something specific to C1
public:
virtual void handle() { do what has to be done for C1 }
};
class C2 : public CBase { ... something specific to C2
public:
virtual void handle() { do what has to be done for C2 }
};

现在,听起来这有点像已经解决了这个问题,因为您可以调用C1.handle()或CBase->handle()。如果需要通过外部函数完成,可以执行以下操作:

void handle(CBase *base_ptr) {
base_ptr->handle();
}

就个人而言,我觉得这比通过非常量引用更好,但我知道这是一个大辩论。

关于您的SFINAE: SFINAE 只是说,如果模板替换失败,编译器将继续寻找匹配项,而不是直接抛出错误。但是如果根本找不到匹配项,它将引发错误。在您的情况下,例如,如果 T = C2,它仍将尝试编译 handle_C1(C2),这会失败。(您的"if case"是运行时决策,而编译器在编译时做出这些决定)

您可以恢复到旧的指针别名。允许将指向一个类型的指针强制转换为指向另一个类型的指针,如果指向的值没有正确的类型,则只需 UB 取消引用指针。

但在这里你知道,在运行时,类型是正确的,因为std::is_same保证了它。所以我会写:

template<typename T>
void handle(T &content){
// Initialization was done here
if (std::is_same<C1, T>()) {
C1* c = reinterpret_cast<C1*>(&content);
handle_C1(*c);
}
if (std::is_same<C2, T>()) {
C2* c = reinterpret_cast<C2*>(&content);
handle_C2(*c);
}
}

它只是花费自动指针分配和复制,一个真正聪明的优化编译器可以避免,因为在低级机器代码中,它显然是一个无操作。

这是另一种解决方案,它只是将两种行为共有的代码提取到自己的函数中:

class C1 { /* ... */ };
class C2 { /* ... */ };
template<typename T>
void handle_init(T& content) {
// Common initialization code
}
void handle(C1& c1) {
handle_init(c1);
// C1-specific code
}
void handle(C2& c2) {
handle_init(c2);
// C2-specific code
}

根据handle_init内部的代码,如果函数的主体只调用content的"getter"方法,则模板可能不是必需的,而这些方法可以事先调用并由handle函数传递给handle_init

handle命名这两个函数是为了与您提供的代码保持一致,但您可以为它们指定不同的名称,它仍然适用于大多数用例。重载的大多数用法都是这种情况,因为重载解析使用参数的编译时类型,作为程序员,您通常在调用站点上知道这一点。

一个很大的例外是,如果要handle的参数是模板参数,在这种情况下,您需要使用重载(因此,其他一些答案实际上确实需要重载)。请注意,要根据参数的运行时类型调用不同的函数,重载不起作用,您需要像 Cedric 的答案那样的虚拟方法。

但除了上面的段落之外,拥有一个处理不同类型的参数的函数与为每个参数类型使用不同的函数在功能上并没有真正的区别。当然,也许在你的问题领域,将其视为前者是有意义的。使用重载handle调用这两个函数将提供符号上的便利性。我只是认为在执行此操作时有意识地意识到这一点是件好事,以防事实证明重载没有您需要的功能。