默认构造函数省略/赋值省略原则上可行吗

Is default constructor elision / assignment elision possible in principle?

本文关键字:原则上 赋值省 构造函数省 默认      更新时间:2023-10-16

。或者甚至被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)。