c++ - C++14和C++17的默认构造函数调用区别
问题描述
考虑以下代码(问题如下):
#include <iostream>
struct Type0
{
Type0(char* c)
{}
};
struct Type1
{
Type1(int* i=nullptr) : i_(i)
{}
Type1(const Type1& other) = default;
int* i_;
};
template <typename ...>
struct Composable;
template <typename T0, typename ... T>
struct Composable<T0, T...> : T0, Composable<T...>
{
Composable()
{
std::cout << "Default Invoked: " << sizeof...(T) << std::endl;
}
Composable(const Composable& other) = default;
template<typename Arg, typename ... Args>
Composable(Arg&& arg, Args&& ... args) :
T0(std::forward<Arg>(arg)), Composable<T...>(std::forward<Args>(args)...)
{
std::cout << "Non-default invoked: " << sizeof...(T) << std::endl;
}
};
template <>
struct Composable<>{};
int main()
{
int i=1;
char c='c';
auto comp = Composable<Type0, Type1>(&c, &i);
std::cout << comp.i_ << std::endl;
}
您可以在此处找到实时代码。此代码有一个有趣的属性:根据您是否使用--std=C++17
or--std=C++14
选项编译它,行为会发生变化(您可以在我的实时代码链接中尝试此操作:编辑--std
左下角的 g++ 调用参数)。
使用--std=c++14
,您将获得以下输出:
Non-default invoked: 0
Non-default invoked: 1
Default Invoked: 0
Non-default invoked: 1
0x0
使用--std=C++17
,你会得到这个:
Non-default invoked: 0
Non-default invoked: 1
0x7ffcdf02766c
对我来说,这种差异令人费解。似乎很清楚,C++17 版本做的是正确的事情,而 C++14 是错误的。C++14 版本调用Composable
和(从它)的默认构造函数Type1
(这是0x0
最后一行输出的来源,因为Type1
它提供了 this 作为其i
构造函数参数的默认值)。但是,我没有看到应该调用默认构造函数的任何地方。
此外,如果我将默认构造函数全部注释掉Composable
,C++17 版本的功能与以前完全相同,而 C++14 版本现在无法编译,抱怨缺少默认构造函数。如果有任何希望通过不同的优化行为以某种方式解释差异,那么这个事实肯定会扼杀它(希望无论如何都很小,因为观察到的差异在所有优化级别都持续存在,包括 0)。
谁能解释这个区别?C++14 行为是一个错误,还是一些我不理解的预期行为?如果 C++14 的行为在 C++14 的规则内是正确的,有人可以解释一下默认构造函数调用的来源吗?
解决方案
保证复制省略。
这一行:
auto comp = Composable<Type0, Type1>(&c, &i);
在 C++17 中,这意味着与以下内容完全相同:
Composable<Type0, Type1> comp(&c, &i);
如果您更改到此版本,您将在 C++14 和 C++17 之间看到相同的行为。但是,在 C++14 中,这仍然是一个移动构造(或者,在技术上更正确,正如您稍后会看到的,复制初始化)。但是在 中Composable
,您没有隐式生成的移动构造函数,因为您有一个用户声明的复制构造函数。因此,对于移动构造,您的“非默认调用”构造函数模板在 C++14 版本中被调用(它比复制构造函数更匹配):
template<typename Arg, typename ... Args>
Composable(Arg&& arg, Args&& ... args) :
T0(std::forward<Arg>(arg)), Composable<T...>(std::forward<Args>(args)...)
在这里,Arg
是Composable<Type0, Type1>
和Args
是一个空包。我们委托给T0
( Type0
) 的构造函数,转发整个Composable
(因为它是从Type0
公共继承的,所以我们在那里得到隐式生成的移动构造函数)和Composable<Type1>
的默认构造函数(因为args
它是空的)。
这个构造函数模板并不是真正的移动构造函数——它根本不会初始化Type1
成员。而不是从右侧移动,Type1::i_
你调用Type1::Type1()
的是默认构造函数,这就是你最终得到0
.
如果添加正确的移动构造函数:
Composable(Composable&& other) = default;
然后你会再次看到 C++14 和 C++17 之间的相同行为。