【问题标题】:RVO with a standard layout struct without any constructors具有标准布局结构的 RVO,没有任何构造函数
【发布时间】:2015-06-09 15:50:29
【问题描述】:

我有一个表示二进制消息的结构。我想编写一个函数来从缓冲区中获取下一条这样的记录(无论是文件还是套接字,都无所谓):

template <typename Record>
Record getNext();

现在,我可以这样写:

template <typename Record>
Record getNext() {
    Record r;
    populateNext(reinterpret_cast<char*>(&r),  // maybe ::read()
                 sizeof(r));                   // or equivalent
    return r;
}

这很好,让我受益于 RVO。但是,它将调用Record 的默认构造函数,它可能由具有非平凡默认构造函数的类型组成,这些构造函数确实可以避免我想避免的工作 - 这些不一定是 POD 类型,但它们是 em> 标准布局。

有没有办法编写getNext(),这样我们就可以避免Record 上的任何构造函数(默认或复制/移动)?理想情况下,当用户调用时:

auto record = getNext<Record>();

缓冲区直接读入record的内存。这可能吗?

【问题讨论】:

  • 出于好奇,您为什么要避免通过引用传递记录?实施要求?
  • 可以说,您需要 trivially copyable 从字节数组复制到对象(以恢复其值而不是 UB)。如果对象是可简单复制的,则编译器可能会省略复制 ctor,即使它不属于复制省略异常。
  • @Columbo 我真的不知道。取决于您对 fine 的含义。我确信它需要使用 trivially copyable 类型。有讨论说这里trivially copyable太严格了,具体细节我记不得了。
  • @dyp Pablo 没有从 Chandler 那里听到任何关于磁盘调配的消息,他的想法只是关于破坏性移动优化。他说 noop 构造函数已经死了,他无意恢复这个话题。

标签: c++ c++11 rvo


【解决方案1】:

no_initno_init_t 类型的常量。

如果你从 no_init_t 构造一个 pod,你会得到一个未初始化的 pod,并且(假设省略)没有什么可做的。

如果你从 no_init_t 构造一个非 Pod,你必须重写一个构造函数,并且让它不初始化数据。通常class_name(no_init_t):field1(no_init), field2(no_init){} 会这样做,有时class_name(no_init_t){} 会这样做(假设所有内容都是 pod)。

在每个成员上从 no_init 构造可以作为成员确实是 pod 的健全性检查。在您编写 no_init_t 构造函数之前,从 no_init 构造的非 pod 类将无法编译。

这(必须no_init每个成员构造函数)确实会产生一些烦人的 DRY 失败,但我们没有得到反思,所以你会重复自己并喜欢它。

namespace {
  struct no_init_t {
    template<class T, class=std::enable_if_t<std::is_pod<T>{}>>
    operator T()const{
      T tmp;
      return tmp;
    }
    static no_init_t instance() { return {}; }
    no_init_t(no_init_t const&) = default;
  private:
    no_init_t() = default;
  };
  static const no_init = no_init_t::instance();
}


struct Foo {
  char buff[1000];
  size_t hash;
  Foo():Foo(""){}
  template<size_t N, class=std::enable_if_t< (N<=sizeof(buff)) >>
  Foo( char const(&in)[N] ) {
    // some "expensive" copy and hash
  }
  Foo(no_init_t) {} // no initialization!
};
struct Record {
  int x;
  Foo foo;
  Record()=default;
  Record(no_init_t):
    x(no_init), foo(no_init)
  {}
};

现在我们可以用no_init构造Record,它不会被初始化。

每个 POD 类都没有初始化。每个非 POD 类都必须提供一个 no_init_t 构造函数(并尽可能实现非初始化)。

然后你 memcpy 就在它上面。

这需要修改您的类型及其包含的类型,以支持非初始化。

【讨论】:

  • 这真的很有趣。你为什么要把no_init_t弄成这样私建?
  • @barry 你应该使用no_init 名称,而不是no_init_t{}。基本上,模仿nullptr_t
  • 我明白了——我的意思是为什么不喜欢static constexpr no_init_t no_init;
  • @Barry 因为那样no_init_t{} 就可以了。我在模仿nullptr_t,其中nullptr_t{} 不起作用,你必须使用nullptr
  • @Yakk Are you sure about nullptr_t{} not working? (我也想知道返回未初始化的东西在技术上是否是UB。)
【解决方案2】:

这样的?

编辑:

  1. 解决对齐注释问题。现在使用匿名联合来确保正确对齐。

  2. TestRecord 现在合并了另一种标准布局类型egg

  3. 添加了证明,即使egg 具有默认构造函数,该类在被populateNextRecord() 填充之前并未构造

我认为这是尽可能快的,不是吗?

#include <iostream>
#include <array>
#include <algorithm>

struct egg {
    egg(int i) : _val(i) {}
    egg() {}
    int _val = 6;    
    friend std::ostream& operator<<(std::ostream& os, const egg& e) {
        return os << e._val; 
    }
};

struct TestRecord {
    egg x;
    double y;
};

void populateNext(uint8_t* first, size_t length)
{
    // do work here
    TestRecord data_source { 10, 100.2 };
    auto source = reinterpret_cast<uint8_t*>(&data_source);
    std::copy(source, source + length, first);
}

template<class Record>
struct RecordProxy
{
    RecordProxy() {}

  uint8_t* data() {
      return _data;
  }

  static constexpr size_t size() {
      return sizeof(Record);
  }

  Record& as_record() {
      return _record;
  }

    union {
        Record _record;
        uint8_t _data[sizeof(Record)];
    };
};


template <typename Record>
RecordProxy<Record> getNext() {
    RecordProxy<Record> r;
    populateNext(r.data(),  // maybe ::read()
                 r.size());                   // or equivalent
    return r;
}

using namespace std;
int main()
{
    RecordProxy<TestRecord> prove_not_initialised;
    auto& r1 = prove_not_initialised.as_record();
    cout << "x = " << r1.x << ", y = " << r1.y << endl;

    auto buffer = getNext<TestRecord>();
    auto& actual_record = buffer.as_record();
    cout << "x = " << actual_record.x << ", y = " << actual_record.y << endl;
   return 0;
}

【讨论】:

  • 至少std::aligned_storage吧!
  • @Yakk 最新编辑地址对齐注释并添加了构造函数未被调用的证明。
  • 这会导致未定义的行为 - 只有最后写入的联合成员可以在 C++ 中读取
  • @MattMcNabb 虽然严格阅读标准支持这个想法,但我想你会发现这种行为是可以预测的,因为联合中所有项目的地址都是相同的,并且在最低公共对齐边界
  • @MattMcNabb 将std::addressof(_record) 转换为char* 并写信给它。我认为别名规则明确允许此类操作?
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2021-06-17
相关资源
最近更新 更多