7

. 甚至是 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 的内存?还是直接在d的内存中构造get_data的返回类型?
我想不出不允许这种优化的理由。至少对于只有 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 默认 operator= 的类进行优化。

4

4 回答 4

10

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

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

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

于 2013-09-04T09:34:13.273 回答
4

标准是否允许省略赋值运算符?与建筑的方式不同。如果您有任何构造d = ...,则将调用赋值运算符。如果...产生与 相同类型的表达式,d则将调用适当的复制或移动赋值运算符。

理论上讲,可以省略一个简单的复制/移动赋值运算符。但是不允许实现省略任何您可以检测到被省略的内容。

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

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

该类的对象将保存足够的数据(例如 20 个双精度数据),因此必须将副本保持在最低限度。

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

所以这真的取决于你想要复制多少。

于 2013-09-04T10:47:12.577 回答
2

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

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

于 2013-09-04T09:41:43.740 回答
2

Jan Hudec回答对as-if规则有一个很好的观点(他为 +1)。

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

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

于 2013-09-04T09:45:12.967 回答