我认为问题在于
template<class ...Args>
C(Args&& ... args) {std::cout << "Ctr\n";}
就语言而言,它们不是复制/移动构造函数,因此编译器不能省略对它们的调用。从 §12.8 [class.copy]/p2-3 开始,添加了重点并省略了示例:
如果类的第一个参数是、或类型,并且没有其他参数或者所有其他参数都具有默认参数(8.3.6),则类的非模板构造函数是一个复制构造函数。X
X&
const X&
volatile X&
const volatile X&
类的非模板构造函数X
是移动构造函数,如果其第一个参数的类型为X&&
、const X&&
、volatile X&&
或
const volatile X&&
,并且要么没有其他参数,要么所有其他参数都有默认参数(8.3.6)。
换句话说,作为模板的构造函数永远不能是复制或移动构造函数。
返回值优化是复制省略的一种特殊情况,描述为 (§12.8 [class.copy]/p31):
当满足某些条件时,允许实现省略类对象的复制/移动构造,即使为复制/移动操作选择的构造函数和/或对象的析构函数具有副作用。
这允许实现省略“复制/移动构造”;使用既不是复制构造函数也不是移动构造函数的东西构造对象不是“复制/移动构造”。
因为C
具有用户定义的析构函数,所以不会生成隐式移动构造函数。因此,重载决议将选择Args
推导为的模板化构造函数C
,这比右值的隐式复制构造函数更匹配。但是,编译器不能省略对这个构造函数的调用,因为它有副作用,既不是复制构造函数也不是移动构造函数。
如果模板化的构造函数是
template<class ...Args>
C(Args ... args) {std::cout << "Ctr\n";}
然后它不能用Args
=实例化C
来产生一个复制构造函数,因为这会导致无限递归。标准中有一条特殊规则禁止此类构造函数和实例化(§12.8 [class.copy]/p6):
X
如果类的第一个参数是类型(可选 cv 限定)X
并且没有其他参数或者所有其他参数都具有默认参数,则类的构造函数声明是格式错误的。永远不会实例化成员函数模板来生成这样的构造函数签名。
因此,在这种情况下,唯一可行的构造函数将是隐式定义的复制构造函数,并且可以省略对该构造函数的调用。
如果我们改为从中删除自定义析构函数C
,并添加另一个类来跟踪何时C
调用 的析构函数:
struct D {
~D() { std::cout << "D's Dstr\n"; }
};
template<class ...ArgsIn>
struct C {
template<class ...Args>
C(Args&& ... args) {std::cout << "Ctr\n";}
D d;
};
我们只看到一次调用D
's 的析构函数,表明只C
构造了一个对象。这里C
的 move 构造函数是由重载决议隐式生成和选择的,你会看到 RVO 再次启动。