为什么标准不将模板构造函数视为复制构造函数?

32
以下是“复制构造函数”的定义,引用自[class.copy.ctor/1]

如果类X的非模板构造函数的第一个参数为X&、const X&、volatile X&或const volatile X&,并且要么没有其他参数,要么所有其他参数都有默认参数,则该构造函数是X的复制构造函数。

为什么标准将模板排除在复制构造函数之外?
在这个简单的例子中,两个构造函数都是复制构造函数:
struct Foo {
    Foo(const Foo &); // copy constructor
    Foo(Foo &);       // copy constructor
};

看下面这个类似的例子:

struct Foo {
     Foo() = default;

     template <typename T>
     Foo(T &) {
         printf("here\n");
     }
};

int main() {
    Foo a;
    Foo b = a;
}

在这个例子中,将打印出here。因此,我的模板构造函数是一个复制构造函数,至少它表现得像一个(它在通常调用复制构造函数的上下文中被调用)。
为什么文本中有“非模板”要求?

1
如果你执行Foo c = std::move(a);会发生什么? - Caleth
请参见 https://stackoverflow.com/a/24832212/3370124 - Richard Critten
1
@Caleth 不错的测试:http://coliru.stacked-crooked.com/a/cff8c0b034585446 什么也没有被打印出来(gcc trunk) - YSC
1
“复制构造函数”具有特殊的地位。它不仅仅是可以用于复制的任何旧构造函数。 - molbdnilo
2
@sebrockm:标准规定了复制构造函数的定义。它规定,如果构造函数是非模板的,并具有特定的参数,则它是一个复制构造函数。所有其他构造函数都不是复制构造函数。 - geza
显示剩余4条评论
3个回答

31
让我们暂且不谈模板。如果一个类没有声明复制构造函数,那么会生成一个隐式默认的构造函数。它可能被定义为删除,但仍然是默认的。
成员模板不是成员函数。只有在需要时才从中实例化成员。
那么编译器如何仅从类定义中知道是否需要使用 T = Foo 的特化呢?它不能。但正是这个问题决定了如何处理潜在需要隐式默认复制构造函数(和移动构造函数)。这变得混乱起来。
最简单的方法是排除模板。我们总会有一些复制构造函数,它会默认执行正确的操作,并且由于它不是从模板实例化而来,所以会受到重载分辨率的青睐。

3
你对在问题中展示的程序中,g++和clang++打印“here”的做法有何看法? - YSC
6
基本上,您的意思是这是因为规则:“如果类定义没有明确声明复制构造函数,则会隐式声明一个非显式构造函数”。而我们仍然期望通常的隐式复制构造函数被定义,无论模板化构造函数如何。是的,这似乎是解释。 - geza
2
@YSC - 老实说,这对我来说感觉像是一个 bug。我现在无法进行更多的实验以发表更多评论。这是从我的手机上发出的。 - StoryTeller - Unslander Monica
3
@YSC - 或者......默认函数有一个带const限定符的参数,而模板没有。因此,它更匹配。如果使用T const&仍然会发生这种情况吗? - StoryTeller - Unslander Monica
8
@YSC: 它被打印出来是因为传递的参数不是const,所以模板比隐式的更匹配。但是对我来说,不称之为复制构造函数会产生困惑。但标准之所以这样做是因为StoryTeller的解释。至少,这是一个合理的解释。 - geza
显示剩余2条评论

10
为什么文本中有“非模板”要求?
如果不这样规定,拷贝构造函数可能会成为模板。在存在拷贝构造函数模板的情况下,非拷贝构造函数如何不会产生歧义呢?考虑以下代码:
struct Foo {
   // ctor template: clearly useful and necessary
   template <typename T>
      Foo(const T&) {}

   // copy ctor: same signature! can't work
   template <typename T>
      Foo(const T &) {}
};

此外,从不是Foo的对象构造Foo可以通过转换或普通构造实现,但允许从非Foo对象进行复制构造将“复制”的概念变为“包括转换的复制”。但是,这已经可以使用现有方案(转换或非复制构造)来实现。

在这个例子中,将打印出"here"。因此,似乎我的模板构造函数是一个复制构造函数。

你展示的例子并没有调用复制构造函数,而是普通的隐式构造函数。如果你把构造函数模板改成:

template <typename T>
Foo(const T &) {
//  ^^^^^
    printf("here\n");
}

当执行Foo b = a;时,会调用编译器生成的复制构造函数。请注意,编译器生成的复制构造函数具有以下签名:

Foo(const Foo&);

这需要在Foo b = a;中给a添加一个const限定符。在您的代码片段中,原始构造函数模板Foo(T&)是更好的匹配,因为没有添加const限定符。

4
啊,没错!确实是整个const和non-const的问题。+1 - StoryTeller - Unslander Monica
我不理解你回答的第一部分。你是想在两个构造函数中都使用“template”吗?我认为逻辑上来说,如果一个构造函数被特化为“T=Foo”,那么它就是一个拷贝构造函数。 - geza
两个“template”都是有意义的,我试图表达的是,如果你有一个普通的构造函数模板(因为它很有用),但你还需要实现类的复制构造函数(因为它管理资源),它们不能共存,但它们也不能成为一个实体,因为它们有不同的目的。不可否认,我理解你的观点,但是当你使用Foo实例化一个通用的构造函数模板时,它是否能够做正确的事情呢?如果你需要特化它,那么这只是非模板复制构造函数,对吗? - lubgr
@lubgr:抱歉,我的意思是“使用T=Foo实例化”(因此不是显式特化)。最近在Stack Overflow上阅读答案后,我对这些术语感到困惑。我认为我需要阅读有关此事的标准(我记得我们所谓的实例化,标准称之为隐式(实例化)特化)。 - geza

0
一个复制构造函数的形式是X(X&)或(X const&),如果您没有声明自己的复制构造函数,编译器会为您提供。非模板可能出现在这里,可能是由于使用模板类时出现问题。
假设有一个具有模板复制构造函数的模板类。问题在于,当您使用具有相同模板类型的此类的另一个实例来实例化该类时,将不会调用您的模板复制构造函数。
问题不在于您的复制构造函数模板不匹配。问题在于隐式复制构造函数不是函数模板,并且在重载分辨率方面,非模板优先于模板特化。
来源:C++ template copy constructor on template class

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