NRVO 是否发生在静态成员变量初始化中?

Does NRVO happen in static member variables initialization?

我有一个 class 和一个大静态 std::array 需要昂贵的计算来初始化,所以我定义了一个静态方法来执行它。但是我不知道是否发生了一些复制或者计算是在成员数组内部执行的。

class A
{
  static inline array<double, 100000> a = fill_a();
  static array<double, 100000> fill_a() 
  {
    array<double, 100000> b;  
    /* large computation involving b with non constexpr functions */
    return b;
  }
};

如何测试 b 是否真的被复制了?或者如果一切都在一个里面完成? 感谢您的帮助或提示。

您将需要在此处测试您的编译器,因为并非所有编译器都会删除副本,它们是否这样做可能取决于优化设置。

有两种方法可以做到这一点,我建议在优化的构建中同时使用这两种方法。

  1. 检查生成的汇编代码,看大数组是在静态存储中构建的,还是先作为堆栈临时复制。

  2. 将数组包装在一个 class 中,其复制构造函数包含日志记录,使用该日志记录 class 的对象而不是数组,并检查是否有任何副本制作。

#2 应该不会超过 5 分钟来测试。 #1 更复杂,但如果您从事的是对性能敏感的工作,那么这是一项值得学习的技能。

这是我对 #2 的设想:

#include<array>
#include<algorithm>
using namespace std;

struct S
{
    array<double, 100000> a;
    S() { puts("ctor"); }
    S(S const& rhs) : a(rhs.a) { puts("copy ctor"); }
    S(S const&& rhs) : a(rhs.a) { puts("move ctor"); }
};

class A
{
  static S fill_a() 
  {
    S b;  
    /* large computation involving b with non constexpr functions */
    return b;
  }
  inline static S a = fill_a();
};

int main()
{
}

我不知道“NRVO”是什么。从上下文我猜测你指的是复制省略。在少数情况下允许复制省略。特别是,当 return 与函数的 return 类型具有相同类型的非 volatile 局部变量时,复制省略是 允许的 (参见[class.copy.elision] 第 1.1 段。

对于 C++17,复制省略在某些情况下成为强制性的。然而,在所有这些情况下,省略的副本将复制一个 prvalue ,这显然不是命名变量的情况。因此,编译器 可以 删除副本,但 没有强制要求 这样做。这是实施质量问题。

由于复制省略的效果是所讨论的对象是在目标位置构建的,并且目标位置是已知的(在本例中 &S::b),因此应该可以 assert()不使用复制省略的情况,例如:

#include <algorithm>
#include <cassert>
#include <array>

struct S
{   
    static std::array<double, 10> fill_a() {
        std::array<double, 10> b;
        assert(&a == &b);
        std::fill(b.begin(), b.end(), 0); 
        return b;
    }   
    static inline std::array<double, 10> a = fill_a();
};  

int main()
{   
    return S::a[0]; // reference S::a
}

有趣的是,上面的代码 assert() 虽然我认为 g++ 通常在这些情况下实现复制省略。不幸的是,添加 assert(&a == &b) 或更准确地说是获取 returned 对象的地址很有可能会抑制复制省略。

否则,有必要观察复制的副作用以确定是否没有发生复制省略。使用 std::array<double, N> 没有任何明显的副作用。不过,可以使用自定义 class,例如:

#include <algorithm>
#include <iostream>
#include <cassert>
#include <cstdlib>
#include <array>

struct C {
    int i = 0;
    C() = default;
    C(C &&){ std::cout << "moved\n"; };
    C(C const&){ std::cout << "copied\n"; };
};
struct S
{
    static std::array<C, 10> fill_a() {
        std::array<C, 10> b;
        c = &b;
        // assert(&a == &b);
        return b;
    }
    static inline std::array<C, 10>  a = fill_a();
    static inline std::array<C, 10>* c = nullptr;
};

int main()
{
    assert(S::c == &S::a);
    return S::a[0].i; // reference S::a
}

这不会打印任何有关复制或移动 C 对象的信息,即使局部变量的地址已被占用,甚至 main() 中的 assert() 也成立。但是,当取消注释 fill_a() 中的断言时,它会触发!可以通过使复制省略成为不可能来验证副作用会做正确的事情,例如,使用

std::array<C, 10> fill_a() {
    std::array<C, 10> b, d;
    return std::rand() % 2? b: d;
}