默认构造函数省略/赋值省略原则上可行吗
Is default constructor elision / assignment elision possible in principle?
。或者甚至被C++11标准所允许?
如果是这样的话,有没有编译器真的能做到这一点?
下面是我的意思的一个例子:
template<class T> //T is a builtin type
class data
{
public:
constexpr
data() noexcept :
x_{0,0,0,0}
{}
constexpr
data(const T& a, const T& b, const T& c, const T& d) noexcept :
x_{a,b,c,d}
{}
data(const data&) noexcept = default;
data& operator = (const data&) noexcept = default;
constexpr const T&
operator[] (std::size_t i) const noexcept {
return x_[i];
}
T&
operator[] (std::size_t i) noexcept {
return x_[i];
}
private:
T x_[4];
};
template<class Ostream, class T>
Ostream& operator << (Ostream& os, const data<T>& d)
{
return (os << d[0] <<' '<< d[1] <<' '<< d[2] <<' '<< d[3]);
}
template<class T>
inline constexpr
data<T>
get_data(const T& x, const T& y)
{
return data<T>{x + y, x * y, x*x, y*y};
}
int main()
{
double x, y;
std::cin >> x >> y;
auto d = data<double>{x, y, 2*x, 2*y};
std::cout << d << std::endl;
//THE QUESTION IS ABOUT THIS LINE
d = get_data(x,y);
d[0] += d[2];
d[1] += d[3];
d[2] *= d[3];
std::cout << d << std::endl;
return 0;
}
关于标记线:
值x+y、x*y、x**x、y*y可以直接写入d的内存吗?或者get_data的返回类型可以直接在d的内存中构造吗
我想不出有什么理由不允许这样的优化。对于一个只有constexpr构造函数和默认的复制和赋值运算符的类,至少不是这样
g++4.7.2取消了本例中的所有复制构造函数;然而,似乎总是执行赋值(即使只是默认赋值——据我从g++发出的程序集所知)。
我提出这个问题的动机是在以下情况下,这样的优化将极大地简化和改进库的设计。假设您使用一个文字类编写性能关键库例程。该类的对象将保存足够的数据(比如20个doubles),因此副本必须保持在最低限度。
class Literal{ constexpr Literal(...): {...} {} ...};
//nice: allows RVO and is guaranteed to not have any side effects
constexpr Literal get_random_literal(RandomEngine&) {return Literal{....}; }
//not favorable in my opinion: possible non-obvious side-effects, code duplication
//would be superfluous if said optimization were performed
void set_literal_random(RandomEngine&, Literal&) {...}
如果我可以不使用第二个函数的话,这将使设计更加简洁(函数式编程风格)。但有时我只需要修改一个长期存在的Literal对象,并且必须确保我不会创建一个新对象并将其复制分配给我想要修改的对象。修改本身很便宜,复制品却不是——这就是我的实验所表明的。
编辑:
假设优化只允许用于具有noexcept constexpr构造函数和noexcept-default运算符=的类。
仅允许基于一般规则删除默认的复制/移动分配运算符。也就是说,如果编译器能够确定它对行为没有可观察的影响,那么它就可以做到这一点。
在实践中,假设规则以一般方式使用,以允许在中间表示和组装级别进行优化。如果编译器能够内联默认的构造函数和赋值,那么它就可以优化它们。它永远不会使用复制构造函数的代码,但对于它们的默认实现,它最终应该使用相同的代码。
编辑:我在有代码示例之前就回答了。复制/移动构造函数是基于编译器的显式权限来消除的,因此即使它们具有可观察的效果(打印"Copy"),它们也会被消除。赋值只能根据"好像"规则来消除,但它们具有可观察的效果(打印"ASSIGN"),因此编译器不允许触摸它们。
标准是否允许省略赋值运算符?与建筑不同。如果有任何构造d = ...
,则会调用赋值运算符。如果...
产生与d
相同类型的表达式,则将调用相应的复制或移动赋值运算符。
理论上可以省略琐碎的复制/移动赋值运算符。但是,不允许实现消除任何可以检测到被消除的内容。
请注意,这与实际的复制/移动省略不同,因为该标准明确允许省略任何构造函数,无论是否微不足道。您可以按值将std::vector
返回到一个新变量中,如果编译器支持,则副本将被省略。尽管检测省略非常容易。该标准允许编译器执行此操作。
不授予复制/移动分配这样的权限。所以它只能"消除"一些你无法分辨的东西。这并不是真正的"省略";这只是编译器的优化。
该类的对象将保存足够的数据(比如20个doubles),因此副本必须保持在最低限度。
没有什么可以阻止您现在返回Literal
类型。如果将对象存储在新变量中,则会得到省略。如果您将其复制并分配给现有变量,则不会。但这与返回一个存储在现有变量中的浮点值的函数没有什么不同:您获得了浮点值的副本。
所以你想复制多少真的取决于你。
您的建议有一个重要的缺点:如果构造函数抛出,会发生什么?这种情况下的行为在标准中有很好的定义(所有已经构建的数据成员都以相反的顺序进行销毁),但这将如何转化为我们将对象"构建"为现有对象的情况?
您的示例很简单,因为构造函数在T = double
时不能抛出,但在一般情况下情况并非如此。您可能会得到一个半破坏的对象,然后会出现未定义的行为,即使您的构造函数和赋值运算符表现良好。
Jan Hudec的回答对有一个很好的观点,就好像规则一样(对他来说是+1)。
因此,正如他所说,只要没有可观察到的影响,就可以省略赋值。您的赋值运算符输出"ASSIGN"
这一事实足以阻止优化。
请注意,复制/移动构造函数的情况有所不同,因为标准允许省略复制/移动构造器,即使它们有可观察到的副作用(见12.8/31)。
- 如何在双向链表上实现复制赋值?
- 结构化绑定初始值设定项表单 { 赋值表达式 } 对于 clang 上的数组类型失败
- 复制赋值函数如何访问另一个对象的私有成员(Stroustroup 原则和实践书)?
- 在未初始化的变量上使用复合赋值运算符(+=, ..)不是C++中的UB?
- 复制引用结构上的赋值
- 赋值运算符上的双重释放或损坏(out)
- 等价于将c++上的char赋值给c#
- 输入迭代器是否可以仅在赋值的右侧符号上取消引用?
- 为什么在取消引用的指向接口的指针上使用赋值运算符不是编译器错误
- 使赋值运算符在声明上工作
- 在声明上为类赋值不会编译
- 在原始对象上使用惯用(例如 TBB 的 thread_enumerable_specific')移动赋值调用析构函数
- 析构函数是移动ctor/赋值的RHS上唯一调用过的东西吗
- 空的inizalizer_list上的赋值运算符
- 如何通过赋值运算符重载将一个列表复制到另一个列表上?C++
- 默认构造函数省略/赋值省略原则上可行吗
- 错误:从初始值设定项列表向数组赋值;在ubuntu 1004上工作但在14.04上不工作的代码
- 正在数组上调用赋值运算符
- 如何在元组上使用std::get赋值
- 在sparcsolaris上使用gcc为长整型整数赋值