函数模板和“正常”函数之间奇怪的不一致

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&lt;...&gt; @Barry 你的意思是标准规定在函数模板中禁止隐式转换吗? 模板尝试匹配您提供的类型。他们不会尝试扩展为与接受您的论点兼容的其他内容。在这种情况下,即使它确实允许此类转换,使用 nullptr 调用模板仍然会失败,因为无法推断出 Args... 应该包含什么类型。 @ChrisUzdavinis foo&lt;&gt;(nullptr); 也不起作用,尽管 Args 是已知的。 @YanB。显然,在这种情况下它是未知的。 foo&lt;&gt;表示at leastArgs中有0个类型,没有任何意义。 【参考方案1】:

显然,如果函数参数的类型依赖于必须推导的模板参数(因为它未在 &lt;...&gt; 中指定),则在将参数传递给该参数时不会应用隐式转换。

Source:

不参与模板实参的函数参数 推论(例如,如果相应的模板参数是明确的 指定) 会隐式转换为 相应的函数参数(如通常的重载 分辨率)。

可以扩展显式指定的模板参数包 如果有附加参数,则通过模板参数推导:

template<class ... Types> void f(Types ... values);
void g() 
  f<int*, float*>(0, 0, 0); // Types = int*, float*, int

这也解释了为什么foo&lt;&gt;(nullptr); 仍然不起作用。由于编译器试图推断出额外的类型来扩展Args,在这种情况下,foo(nullptr);foo&lt;&gt;(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 &lt;typename... Args&gt; int* foo(std::tuple&lt;Args...&gt; tpl),但是当这样使用它时:foo( 5, 7.0f, 5.0 ); 它不起作用。我猜这是因为decltype( 5, 7.0f, 5.0 )(不管是什么)不是std::tuple&lt;int, float, double&gt; @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/… - 我剪切并粘贴,忘记更改一堆原始代码。我现在会解决这个问题。 :-)

以上是关于函数模板和“正常”函数之间奇怪的不一致的主要内容,如果未能解决你的问题,请参考以下文章

-----------------------奇怪の模板-----------------------

函数模板与类模板

VC6 和 VS2008 之间的模板函数行为

模板类的 C++ 强制转换

用 PHP 制作一个简单的模板引擎

56)函数模板的基本语法