【问题标题】:Is default constructor elision / assignment elision possible in principle?原则上可以省略默认构造函数省略/赋值省略吗?
【发布时间】:2013-09-04 09:21:32
【问题描述】:

。甚至是 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= 的类进行优化。

【问题讨论】:

  • 据我所知,只能省略复制/移动构造。标准的相关部分是12.8/31。我找不到任何关于复制/移动分配省略的类似内容。
  • 我也没有。这就是为什么我很想知道这种优化是否存在任何原则性反对意见。
  • 这就是为什么我添加了“至少对于只有 constexpr 构造函数和默认复制和赋值运算符的类来说不是”。这正是我关心的情况。
  • 一个“小”hacky,但一个“新位置”可以避免复制......

标签: c++ c++11


【解决方案1】:

仅基于一般的 as-if 规则允许省略默认的复制/移动赋值运算符。如果编译器能够确定它不会对行为产生明显的影响,那么编译器就可以做到这一点。

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

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

【讨论】:

  • 是的,好的。也许我应该在示例中删除我的自定义复制 ctor 和赋值运算符。我真正关心的情况是您只有默认的复制 ctors 和 operator=。而且似乎基于 g++ 发出的程序集,g++ 不会忽略临时的分配/构造。
  • @AndréMüller:确实如此。它可以做到这一点,但在实践中,这将是一个非常特殊的规则,需要大量工作才能实现,但收效甚微。因此,临时将被删除的唯一情况是通用“寄存器”分配找到它并且它甚至可能不会尝试非平凡的对象。
  • 小有收获?如果可以在现代编译器中依赖它,它将消除区分就地修改和构造文字类的需要。可能我为了想要这样的功能做了太多的函数式编程。
【解决方案2】:

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

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

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

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

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

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

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

【讨论】:

  • 好吧,你是对的,我应该称之为优化而不是省略。但这正是我关心的情况。我根本不想要任何副本。我想要等效的就地修改,但没有为此编写额外的函数。
  • @AndréMüller:如果对象当前存在的内容不相关,那么您想创建一个 new 对象,而不是覆盖当前的某个对象。这将就地进行施工。如果对象当前存在的内容与计算相关,则必须将其作为参数传递。您不应该仅仅因为它存在就覆盖现有对象。毕竟,复制是什么。
  • 看来我没有把用例说清楚。我有一个长期存在的对象(对模拟的当前状态进行编码)。该对象的成员之一与示例中的成员类似。现在我想通过修改那个成员的数据来更新模拟状态。但我不想创建第二个对象,将其分配给第一个对象,然后销毁第二个对象。我希望编译器对其进行优化,并提供相当于就地修改的功能,这样我就可以使用工厂函数,而不必编写单独的函数进行修改。
  • @AndréMüller:这种优化只能在你的 C++ 代码被翻译成低级语言之后进行。那时它可能会被优化掉。如果你想在 C++ 级别控制它,你唯一的希望是在get_data 中添加一个data&lt;T&gt;&amp; 参数,以便能够声明“所谓的返回值的位置”。这是一个几十年前的伎俩。
【解决方案3】:

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

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

【讨论】:

  • 好点!这正是我感兴趣的一般问题。
【解决方案4】:

Jan Hudecansweras-if 规则有很好的看法(他 +1)。

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

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

【讨论】:

  • 是的,但我关心默认 operator= 的情况。我编辑了我原来的问题,因为自定义 operator= 造成了太多混乱。
猜你喜欢
  • 2014-06-19
  • 1970-01-01
  • 2020-01-11
  • 2023-03-03
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多