传递与转换相关的参数到使用模板参数推导的模板函数调用是坏事吗?
Is passing arguments related by conversion to a template function call that uses template argument deduction bad thing?
AFAIK 为通过转换相对的类型重载函数或需要将某些强制转换应用于参数以匹配最佳匹配的函数调用是糟糕的设计。
void foo(int)
{
std::cout << "foo(int)\n";
}
void foo(float)
{
std::cout << "foo(float)\n";
}
int main()
{
foo(5.3);// ambiguous call
foo(0u); // ambiguous call
}
因为 5.3
是 double 类型,所以它可以同样转换为 float
或 int
因此调用有多个最佳匹配,因此调用是不明确的。在第二次调用中,同样的事情:0u
是 unsigned int
类型,它可以同样转换为 int
或 float
,因此调用是不明确的。
为了消除调用的歧义,我可以使用显式 cast
:
foo(static_cast<float>(5.3)); // calls foo(float)
foo(static_cast<int>(0u)); // calls foo(int)
代码现在可以运行,但它是一个糟糕的设计,因为它打破了函数重载的原则,在该原则中,编译器负责根据传递给它的参数为调用选择最佳匹配函数。
到这里我还好。但是模板参数推导呢? :
*编译器仅对传递给函数模板调用的参数进行少量转换,以推断模板参数的类型。
所以编译器既不应用算术转换也不应用整数提升,而是经常生成一个新版本,该版本最适合调用:
template <typename T>
void foo(T)
{
std::cout << "foo(" << typeid(T).name() << ")\n";
}
int main()
{
foo(5.3); // calls foo(double)
foo(0u); // calls foo(unsigned)
}
现在它工作正常:编译器生成两个版本的 foo
,一个带有 double
,第二个带有 unsigned
。
我关心的事情:将相关类型的参数传递到使用模板参数推导的函数模板中是不是一个坏主意?
还是语言本身的问题?因为编译器生成的版本可以通过转换相对?
Or the problem is in the language itself? Because the compiler generates versions that can be relative by conversion?
好的,它们通过转换相关联。现在编译器应该生成哪一个?您建议使用哪种确定性算法来选择最佳函数来实例化?它应该首先解析整个翻译单元以找出最佳匹配,还是应该仍然从上到下解析并保持“运行 最佳”功能?
现在,假设这些问题得到了回答。当您稍微修改代码时会发生什么?如果包含实例化更好函数的 header 会怎样?您并没有真正更改 您的 代码,但它的行为仍然发生了变化,可能变化非常大。
考虑到语言的设计难题,以及这种行为可能给毫无戒心的代码带来的潜在混乱,尝试让编译器这样做是一个非常糟糕的主意。
所以不,这不是语言问题。当前的行为确实是最明智的选择,即使它并不总是我们想要的,但这是我们可以学习期望的东西。
The thing that matters me[sic]: Is it a bad idea to pass arguments of related types into a function template that uses template argument deduction for its arguments?
无法对所有情况进行笼统的回答。没有灵丹妙药。这可能正是您的重载集需要做的。或者可能是您需要构建一组更精细的函数(模板),通过 SFINAE 或更现代的技术与重载解析交互。例如,您可以在 C++20
中执行此操作
template <std::integral I>
void foo(I)
{
//
}
template <std::floating_point F>
void foo(F)
{
//
}
这些概念限制每个模板仅适用于特定的系列 类型。这是构建您在第一个示例中想要的重载集、避免歧义并使用与设计模板完全相同的类型的一种方法。
在您的第一个程序中包含 foo
的重载集以及 int
和 float
,语言规则规定调用如下:
foo(5.3);
foo(0u);
模棱两可。重载解析规则表明,在这两个调用中,函数参数匹配参数所需的转换是绑定的,导致两个候选对象同样匹配。
您使用单个函数模板的解决方案可能有效,具体取决于您的用例。一个潜在的问题是,每次使用不同的参数类型调用 foo
都会导致 foo
的完全不同的实例化。另一个是此函数模板将接受 foo
定义有效的任何参数类型。这些行为可能都不是您想要的。
根据您在第一个程序中设置的重载,以及您代码中显式调用特定版本的 static_cast
,看来您实际上想要将 int
和 [=20= 分组],即整数类型归为一类,float
和 double
,即浮点类型归为另一类。您可以通过多种不同的方式实现这一点,例如通过提供涵盖您关心的所有类型的详尽重载集。
我推荐的方法是使用两个函数模板实现重载集,每个模板允许一个类型族。从 C++20 开始,你可以这样实现它:
void foo(std::integral auto)
{
std::cout << "foo(integral)\n";
}
void foo(std::floating_point auto)
{
std::cout << "foo(floating_point)\n";
}
您也可以在 C++20 之前实现相同的效果,使用更多的语法,并使用 SFINAE 和其他模板元编程技术。
这是一个demo。
AFAIK 为通过转换相对的类型重载函数或需要将某些强制转换应用于参数以匹配最佳匹配的函数调用是糟糕的设计。
void foo(int)
{
std::cout << "foo(int)\n";
}
void foo(float)
{
std::cout << "foo(float)\n";
}
int main()
{
foo(5.3);// ambiguous call
foo(0u); // ambiguous call
}
因为 5.3
是 double 类型,所以它可以同样转换为 float
或 int
因此调用有多个最佳匹配,因此调用是不明确的。在第二次调用中,同样的事情:0u
是 unsigned int
类型,它可以同样转换为 int
或 float
,因此调用是不明确的。
为了消除调用的歧义,我可以使用显式
cast
:foo(static_cast<float>(5.3)); // calls foo(float) foo(static_cast<int>(0u)); // calls foo(int)
代码现在可以运行,但它是一个糟糕的设计,因为它打破了函数重载的原则,在该原则中,编译器负责根据传递给它的参数为调用选择最佳匹配函数。
到这里我还好。但是模板参数推导呢? :
*编译器仅对传递给函数模板调用的参数进行少量转换,以推断模板参数的类型。
所以编译器既不应用算术转换也不应用整数提升,而是经常生成一个新版本,该版本最适合调用:
template <typename T> void foo(T) { std::cout << "foo(" << typeid(T).name() << ")\n"; } int main() { foo(5.3); // calls foo(double) foo(0u); // calls foo(unsigned) }
现在它工作正常:编译器生成两个版本的
foo
,一个带有double
,第二个带有unsigned
。我关心的事情:将相关类型的参数传递到使用模板参数推导的函数模板中是不是一个坏主意?
还是语言本身的问题?因为编译器生成的版本可以通过转换相对?
Or the problem is in the language itself? Because the compiler generates versions that can be relative by conversion?
好的,它们通过转换相关联。现在编译器应该生成哪一个?您建议使用哪种确定性算法来选择最佳函数来实例化?它应该首先解析整个翻译单元以找出最佳匹配,还是应该仍然从上到下解析并保持“运行 最佳”功能?
现在,假设这些问题得到了回答。当您稍微修改代码时会发生什么?如果包含实例化更好函数的 header 会怎样?您并没有真正更改 您的 代码,但它的行为仍然发生了变化,可能变化非常大。
考虑到语言的设计难题,以及这种行为可能给毫无戒心的代码带来的潜在混乱,尝试让编译器这样做是一个非常糟糕的主意。
所以不,这不是语言问题。当前的行为确实是最明智的选择,即使它并不总是我们想要的,但这是我们可以学习期望的东西。
The thing that matters me[sic]: Is it a bad idea to pass arguments of related types into a function template that uses template argument deduction for its arguments?
无法对所有情况进行笼统的回答。没有灵丹妙药。这可能正是您的重载集需要做的。或者可能是您需要构建一组更精细的函数(模板),通过 SFINAE 或更现代的技术与重载解析交互。例如,您可以在 C++20
中执行此操作template <std::integral I>
void foo(I)
{
//
}
template <std::floating_point F>
void foo(F)
{
//
}
这些概念限制每个模板仅适用于特定的系列 类型。这是构建您在第一个示例中想要的重载集、避免歧义并使用与设计模板完全相同的类型的一种方法。
在您的第一个程序中包含 foo
的重载集以及 int
和 float
,语言规则规定调用如下:
foo(5.3);
foo(0u);
模棱两可。重载解析规则表明,在这两个调用中,函数参数匹配参数所需的转换是绑定的,导致两个候选对象同样匹配。
您使用单个函数模板的解决方案可能有效,具体取决于您的用例。一个潜在的问题是,每次使用不同的参数类型调用 foo
都会导致 foo
的完全不同的实例化。另一个是此函数模板将接受 foo
定义有效的任何参数类型。这些行为可能都不是您想要的。
根据您在第一个程序中设置的重载,以及您代码中显式调用特定版本的 static_cast
,看来您实际上想要将 int
和 [=20= 分组],即整数类型归为一类,float
和 double
,即浮点类型归为另一类。您可以通过多种不同的方式实现这一点,例如通过提供涵盖您关心的所有类型的详尽重载集。
我推荐的方法是使用两个函数模板实现重载集,每个模板允许一个类型族。从 C++20 开始,你可以这样实现它:
void foo(std::integral auto)
{
std::cout << "foo(integral)\n";
}
void foo(std::floating_point auto)
{
std::cout << "foo(floating_point)\n";
}
您也可以在 C++20 之前实现相同的效果,使用更多的语法,并使用 SFINAE 和其他模板元编程技术。
这是一个demo。