为什么模板构造函数优于复制构造函数?

40
#include <iostream>

struct uct
{
    uct() { std::cerr << "default" << std::endl; }

    uct(const uct &) { std::cerr << "copy" << std::endl; }
    uct(      uct&&) { std::cerr << "move" << std::endl; }

    uct(const int  &) { std::cerr << "int" << std::endl; }
    uct(      int &&) { std::cerr << "int" << std::endl; }

    template <typename T>
    uct(T &&) { std::cerr << "template" << std::endl; }
};

int main()
{
    uct u1    ; // default
    uct u2( 5); // int
    uct u3(u1); // template, why?
}

coliru

构造函数的模板重载适用于两个声明(u2u3)。但是当将int传递给构造函数时,会选择非模板重载。调用复制构造函数时,会选择模板重载。据我所知,在重载决议期间,非模板函数总是优先于模板函数。为什么复制构造函数处理方式不同呢?


8
"过于完美的转发" - aschepler
这个问题中没有任何基于观点的内容。为什么会被视为POB? - L. F.
3个回答

40
据我所知,在重载解析期间,非模板函数总是优先于模板函数。
只有当特化和非模板完全相同时才是如此。但在这种情况下并非如此。当您调用uct u3(u1)时,重载集会出现不同的情况。
uct(const uct &)
uct(uct &) // from the template

现在,既然u1不是const,它需要应用const转换来调用复制构造函数。而为了调用模板特化,它什么都不需要做,因为它是一个精确匹配。这意味着模板胜出,因为它是更好的匹配。

要阻止这种情况,您可以使用SFINAE将模板函数限制为仅在T不是uct时才能调用。代码如下:

template <typename T, std::enable_if_t<!std::is_same_v<uct, std::decay_t<T>>, bool> = true>
uct(T &&) { std::cerr << "template" << std::endl; }

3
补充一下,这就是为什么模板化构造函数是奇怪的东西,除非你想让它们接管所有东西,否则你可能需要标记构造函数(类似于 inplace_t)。 - SergeyA
@SergeyA 或者使用 SFINAE。 - NathanOliver
或者那样做,但标记通常更容易。 - SergeyA
4
添加 const 将前向引用转换为 rvalue 引用。因此,它不是首选方案,原因在于非模板与模板规则之间的区别,而是因为 uct u3(u1) 完全不匹配。 - local-ninja
@fdan 好的,谢谢指出。我已经将那个部分删除了。 - NathanOliver
3
在任何一个使用转发引用的函数中过度载入都是很危险的。 - Ryan Haining

5
当尝试调用复制构造函数时,选择模板重载。据我所知,在重载决议期间,非模板函数始终优先于模板函数。那么为什么复制构造函数会有不同的处理方式呢?
template <typename T>
uct(T &&) { std::cerr << "template" << std::endl; }
//    ^^

模板版本被选中的原因是编译器能够生成一个构造函数签名为 (T &) 的函数,与之匹配更好,所以选择了它。
  • 如果将签名从 uct u1 改为 const uct u1,则它将匹配复制构造函数(因为 u1 最初不是 const)。

  • 如果将签名从 uct(const uct &) 改为 uct(uct&),则匹配更好,编译器会选择它而不是模板版本。

  • 此外,如果使用 uct u3(std::move(u1)),则会选择 uct(uct&&)


要解决这个问题,您可以使用 SFINAE 在 Tuct 相同时禁用重载:
template <typename T, std::enable_if_t<!std::is_same_v<std::decay_t<T>, uct>>>
uct(T&&)
{
  std::cerr << "template" << std::endl;
}

3
问题在于模板构造函数没有修饰符 const,而非模板复制构造函数的参数中有修饰符 const。如果将对象 u1 声明为 const 对象,则会调用非模板复制构造函数。
从C++标准(7个标准转换)来看:

1 标准转换是具有内置含义的隐式转换。第7条列举了这些转换的完整集合。标准转换序列是一个按以下顺序进行的标准转换序列:

(1.4) — 零或一个限定符转换

因此,对于复制构造函数需要进行一个标准转换,而模板构造函数不需要进行这样的转换。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接