函数模板和“正常”函数之间奇怪的不一致
Posted
技术标签:
【中文标题】函数模板和“正常”函数之间奇怪的不一致【英文标题】:Strange inconsistency between function template and "normal" function 【发布时间】:2019-07-11 17:45:59 【问题描述】:我有两个几乎相同的函数(除了其中一个是模板):
int* bar(const std::variant<int*, std::tuple<float, double>>& t)
return std::get<0>(t);
template <typename... Args>
int* foo(const std::variant<int*, std::tuple<Args...>>& t)
return std::get<0>(t);
然后,它们是这样使用的:
foo(nullptr);
bar(nullptr);
第二个编译并返回 (int*)nullptr
,但第一个没有(在 Visual Studio 2019 中使用 C++17 给出错误 foo: no matching overload found
)。为什么?为什么将此函数设为模板会导致它停止编译?
像下面这样使用foo
也无济于事,因此无法推断出Args
可能不是问题:
foo<>(nullptr);
相反,以下方法确实有效:
foo(std::variant<int*, std::tuple<>>(nullptr));
是否有可能以某种方式避免需要写这么长的方式?
【问题讨论】:
模板不进行转换。nullptr
不是 variant<...>
@Barry 你的意思是标准规定在函数模板中禁止隐式转换吗?
模板尝试匹配您提供的类型。他们不会尝试扩展为与接受您的论点兼容的其他内容。在这种情况下,即使它确实允许此类转换,使用 nullptr 调用模板仍然会失败,因为无法推断出 Args... 应该包含什么类型。
@ChrisUzdavinis foo<>(nullptr);
也不起作用,尽管 Args
是已知的。
@YanB。显然,在这种情况下它是未知的。 foo<>
表示at least在Args
中有0个类型,没有任何意义。
【参考方案1】:
显然,如果函数参数的类型依赖于必须推导的模板参数(因为它未在 <...>
中指定),则在将参数传递给该参数时不会应用隐式转换。
Source:
不参与模板实参的函数参数 推论(例如,如果相应的模板参数是明确的 指定) 会隐式转换为 相应的函数参数(如通常的重载 分辨率)。
可以扩展显式指定的模板参数包 如果有附加参数,则通过模板参数推导:
template<class ... Types> void f(Types ... values); void g() f<int*, float*>(0, 0, 0); // Types = int*, float*, int
这也解释了为什么foo<>(nullptr);
仍然不起作用。由于编译器试图推断出额外的类型来扩展Args
,在这种情况下,foo(nullptr);
和foo<>(nullptr);
之间似乎没有任何区别。
【讨论】:
我还是不太明白这一点——它说一个明确指定的模板参数包可以通过模板参数推导来扩展,但我天真地期望如果这样扩展推导失败,编译器应该只将显式指定的参数作为整个包。相反,在 OP 的代码中,我们看到 enclosure 扣除/替换失败。你知道标准中的哪个地方规定了这种行为吗? @Brian 我不知道标准的哪一部分描述了它,但这种行为似乎与 cppreference 引用相匹配。由于形参参与了模板实参推导,因此在向该形参传递实参时不允许隐式转换。 也许我不清楚为什么我会感到困惑 - 显然,尝试扩展参数包的部分推导无法成功,因为没有考虑用户定义的转换。但是,既然推导失败了,为什么编译器不直接说:“好吧,既然扩展参数包失败了,我就假设显式指定的参数构成了整个包”? @Brian ¯\_(ツ)_/¯ 我也看不出有什么好的理由。这可能只是众多 C++ 怪癖之一。【参考方案2】:当考虑模板函数时,它只适用于调用时参数类型的完全匹配。这意味着不会进行任何转换(cv 限定符除外)。
在您的情况下,一个简单的解决方法是让函数 catch std::nullptr_t
并将其转发到您的模板。
int* foo(std::nullptr_t)
return foo(std::variant<int*, std::tuple<>>nullptr);
【讨论】:
感谢您指出这一点,但不幸的是,这仅适用于std::variant
的指针侧,但不适用于 std::tuple
。我尝试使用template <typename... Args> int* foo(std::tuple<Args...> tpl)
,但是当这样使用它时:foo( 5, 7.0f, 5.0 );
它不起作用。我猜这是因为decltype( 5, 7.0f, 5.0 )
(不管是什么)不是std::tuple<int, float, double>
。
@YanB。是的,但这不是您在问题中提出的问题。请用您的新问题按“提问”按钮,而不是在 cmets 中提问。【参考方案3】:
我会避免这种结构只是因为关于编译器将如何(如果它甚至完全做到)解决重载的规则非常混乱,以至于如果不查看标准文档,我无法真正告诉你它做了什么, 并且应该避免这样的代码。我会以这种方式强制您想要的重载解决方案:
template <typename... Args>
int *foo(const ::std::variant<int*, ::std::tuple<Args...>> &t)
return ::std::get<0>(t);
int *foo(int *ip)
using my_variant = ::std::variant<int *, ::std::tuple<>>;
return foo(my_variantip);
template <typename... Args>
int *foo(::std::tuple<Args...> const &t)
using my_variant = ::std::variant<int *, ::std::tuple<Args...>>;
return foo(my_variantt);
template <typename... Args>
int *foo(::std::tuple<Args...> &&t)
using my_variant = ::std::variant<int *, ::std::tuple<Args...>>;
return foo(my_variant::std::move(t));
【讨论】:
这正是我的想法。谢谢!顺便说一句,你为什么要在所有std
成员之前加上范围运算符(std::get
除外)?
@YanB。 - 我向你推荐这个 SO 问题...***.com/questions/1661912/… - 我剪切并粘贴,忘记更改一堆原始代码。我现在会解决这个问题。 :-)以上是关于函数模板和“正常”函数之间奇怪的不一致的主要内容,如果未能解决你的问题,请参考以下文章