原则上默认构造函数elision / assignment elision是否可行?

时间:2013-09-04 09:21:32

标签: c++ c++11

。甚至是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个双打),副本必须保持最小。

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默认运算符=的类进行优化。

4 个答案:

答案 0 :(得分:10)

仅允许基于一般as-if规则的默认复制/移动赋值运算符的删除。也就是说,如果编译器能够确定它对行为没有可观察到的影响,那么编译器就可以做到这一点。

实际上,as-if规则以一般方式使用,以允许在中间表示和汇编级别进行优化。如果编译器可以内联默认构造函数和赋值,它可以优化它们。它不会使用复制构造函数的代码,但对于它们的默认实现,它应该以相同的代码结束。

编辑:我在有代码示例之前回答。复制/移动构造函数基于对编译器的显式许可而被省略,因此即使它们具有可观察的效果(打印“COPY”)也会被省略。只能根据as-if规则省略赋值,但它们具有可观察的效果(打印“ASSIGN”),因此不允许编译器触摸它们。

答案 1 :(得分:3)

标准是否允许赋值运算符的省略?与建筑不同。如果您有任何构造d = ...,则将调用赋值运算符。如果...导致表达式与d相同,则将调用相应的副本或移动赋值运算符。

理论上可能普通复制/移动赋值运算符可以省略。但是实施不允许忽略任何你可以发现被删除的东西。

请注意,这与实际的复制/移动省略不同,因为标准明确允许任何构造函数的省略,无论是否微不足道。您可以将std::vector值返回到新变量中,如果编译器支持该副本,则将省略该副本。即使很容易检测到省略。该标准赋予编译器特殊的权限。

没有授予复制/移动分配的此类权限。所以它只能“消除”你无法区分的东西。这不是真正的“省略”;这只是一个编译器优化。

  

该类的对象将保存足够的数据(比如20个双打),副本必须保持最小。

现在没有什么可以阻止你返回Literal类型。如果将对象存储在新变量中,您将获得省略。如果你复制将它分配给现有变量,你就不会。但这与返回存储到现有变量中的浮点数的函数没什么不同:你得到浮点数的副本。

因此,您需要做多少复制。

答案 2 :(得分:2)

你的建议有一个重要的缺点:如果构造函数抛出会发生什么?该情况的行为在标准中得到了很好的定义(所有已经构建的数据成员都以相反的顺序被破坏),但是如何将我们“构建”对象转换为已经存在的对象呢? / p>

您的示例很简单,因为构造函数不能在T = double时抛出,但在一般情况下情况并非如此。即使构造函数和赋值运算符表现良好,您最终也可能会遇到半被破坏的对象,并且会出现未定义的行为。

答案 3 :(得分:2)

Jan Hudecanswer as-if 规则(+1为他)提出了非常好的观点。

因此,正如他所说的那样,允许任务省略 - 只要没有可观察到的效果。您的赋值运算符输出"ASSIGN"这一事实足以阻止优化。

请注意,复制/移动构造函数的情况不同,因为标准允许省略复制/移动构造函数,即使它们具有可观察到的副作用(参见12.8 / 31)。