当 Base 构造函数依赖于 Derived 的引用时构造函数初始化顺序

发布于 2025-01-11 17:46:04 字数 2294 浏览 0 评论 0原文

我有一个基类,它通过缓冲将对象写入 std::ostream 。我想用 来调用它,

  Obj obj;
  flat_file_stream_writer<Obj> writer(std::cout);
  writer.write(obj);
  writer.flush();

但也用 来

  Obj obj;
  flat_file_writer<Obj> writer("filename.bin");
  writer.write(obj);
  writer.flush();

调用它。后一个调用必须建立一个 std::ofstream 实例,并在编写期间保持它,然后可以调用前一个版本。

我认为简单的非虚拟继承在这里就可以了。但我有构造函数初始化顺序问题。

warning: field 'ofstream_' will be initialized after base
      'flat_file_stream_writer<hibp::pawned_pw>' [-Wreorder-ctor]

因为基类依赖于对派生所持有的对象的引用,这看起来就像是先有鸡还是先有蛋,无法解决?

继承在这里只是错误的抽象吗?一个干净的替代品?

template <typename ValueType>
class flat_file_stream_writer {

  public:
    explicit flat_file_stream_writer(std::ostream& os, std::size_t buf_size = 100)
        : db_(os), buf_(buf_size) {}

    void write(const ValueType& value) {
        if (buf_pos_ == buf_.size()) flush();
        std::memcpy(&buf_[buf_pos_], &value, sizeof(ValueType));
        ++buf_pos_;
    }

    void flush() {  // could also be called in destructor?
        if (buf_pos_ != 0) {
            db_.write(reinterpret_cast<char*>(buf_.data()), // NOLINT reincast
                      static_cast<std::streamsize>(sizeof(ValueType) * buf_pos_));
            buf_pos_ = 0;
        }
    }

  private:
    std::ostream&          db_;
    std::size_t            buf_pos_ = 0;
    std::vector<ValueType> buf_;
};

template <typename ValueType>
class flat_file_writer : public flat_file_stream_writer<ValueType> {
  public:
    explicit flat_file_writer(std::string dbfilename)
        : dbfilename_(std::move(dbfilename)), dbpath_(dbfilename_),
          ofstream_(dbpath_, std::ios::binary), flat_file_stream_writer<ValueType>(ofstream_) {
        if (!ofstream_.is_open())
            throw std::domain_error("cannot open db: " + std::string(dbpath_));
    }

  private:
    std::string                        dbfilename_;
    std::filesystem::path              dbpath_;
    std::ofstream                      ofstream_;
};

I have a base class which writes objects to a std::ostream with buffering. I want to call this with

  Obj obj;
  flat_file_stream_writer<Obj> writer(std::cout);
  writer.write(obj);
  writer.flush();

but also with

  Obj obj;
  flat_file_writer<Obj> writer("filename.bin");
  writer.write(obj);
  writer.flush();

The latter call must establish a std::ofstream instance, and hold that for the duration of writing, and can then call the former version.

I thought simple non-virtual inheritance would be fine here. But I have constructor initialization order issues.

warning: field 'ofstream_' will be initialized after base
      'flat_file_stream_writer<hibp::pawned_pw>' [-Wreorder-ctor]

Because the base class depends on a reference to the object held by the derived this seems like chicken and egg and can't be solved?

Is inheritance just the wrong abstraction here? A clean alternative?

template <typename ValueType>
class flat_file_stream_writer {

  public:
    explicit flat_file_stream_writer(std::ostream& os, std::size_t buf_size = 100)
        : db_(os), buf_(buf_size) {}

    void write(const ValueType& value) {
        if (buf_pos_ == buf_.size()) flush();
        std::memcpy(&buf_[buf_pos_], &value, sizeof(ValueType));
        ++buf_pos_;
    }

    void flush() {  // could also be called in destructor?
        if (buf_pos_ != 0) {
            db_.write(reinterpret_cast<char*>(buf_.data()), // NOLINT reincast
                      static_cast<std::streamsize>(sizeof(ValueType) * buf_pos_));
            buf_pos_ = 0;
        }
    }

  private:
    std::ostream&          db_;
    std::size_t            buf_pos_ = 0;
    std::vector<ValueType> buf_;
};

template <typename ValueType>
class flat_file_writer : public flat_file_stream_writer<ValueType> {
  public:
    explicit flat_file_writer(std::string dbfilename)
        : dbfilename_(std::move(dbfilename)), dbpath_(dbfilename_),
          ofstream_(dbpath_, std::ios::binary), flat_file_stream_writer<ValueType>(ofstream_) {
        if (!ofstream_.is_open())
            throw std::domain_error("cannot open db: " + std::string(dbpath_));
    }

  private:
    std::string                        dbfilename_;
    std::filesystem::path              dbpath_;
    std::ofstream                      ofstream_;
};

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(1

强者自强 2025-01-18 17:46:04

您可以将 std::ofstream ofstream_; 放在单独的结构中,然后让 flat_file_writer 使用 private 从该结构继承。基类按照声明的顺序进行初始化,因此请确保在 flat_file_stream_writer 之前从该结构继承。现在,您可以在基类 flat_file_stream_writer 之前初始化 ofstream_

经过仔细检查,我认为您可能希望将所有 3 个成员放入结构中。

请参阅下面的更新代码。

因此,这实际上意味着 ofstream_holder 中的 3 个成员现在位于 flat_file_writer 布局中的 flat_file_stream_writer 之前。

这似乎是正确的解决方案,不仅因为它“消除了编译器警告”,而且因为现在我们确实获得了正确的初始化顺序,即构造 std::ofstream firstthen 将对它的引用传递给flat_file_stream_writer。在销毁期间,std::ofstream 将被销毁最后

上面的原始代码完全是错误的,因为我们在派生构造函数初始值设定项中编写的顺序被忽略,而实际实现的顺序是布局顺序,即声明成员的顺序。

(尽管原始代码存在缺陷,但它对我来说“运行良好”,带有消毒剂等......但可能是UB?)。

template <typename ValueType>
class flat_file_stream_writer {

  public:
    explicit flat_file_stream_writer(std::ostream& os, std::size_t buf_size = 100)
        : db_(os), buf_(buf_size) {}

    void write(const ValueType& value) {
        if (buf_pos_ == buf_.size()) flush();
        std::memcpy(&buf_[buf_pos_], &value, sizeof(ValueType));
        ++buf_pos_;
    }

    void flush() {
        if (buf_pos_ != 0) {
            db_.write(reinterpret_cast<char*>(buf_.data()), // NOLINT reincast
                      static_cast<std::streamsize>(sizeof(ValueType) * buf_pos_));
            buf_pos_ = 0;
        }
    }

  private:
    std::ostream&          db_;
    std::size_t            buf_pos_ = 0;
    std::vector<ValueType> buf_;
};

struct ofstream_holder {
    explicit ofstream_holder(std::string dbfilename)
        : dbfilename_(std::move(dbfilename)), dbpath_(dbfilename_),
          ofstream_(dbpath_, std::ios::binary) {}

    std::string           dbfilename_;
    std::filesystem::path dbpath_;
    std::ofstream         ofstream_;
};

template <typename ValueType>
class flat_file_writer : private ofstream_holder, public flat_file_stream_writer<ValueType> {
  public:
    explicit flat_file_writer(std::string dbfilename)
            : ofstream_holder(std::move(dbfilename)), flat_file_stream_writer<ValueType>(ofstream_) {
        if (!ofstream_.is_open())
            throw std::domain_error("cannot open db: " + std::string(dbpath_));
    }
};



You can put std::ofstream ofstream_; in a separate struct, then have flat_file_writer inherit from that struct using private. Base classes are initialized in the order they are declared, so make sure to inherit from that struct before flat_file_stream_writer. You can now initialize that ofstream_ before the base class flat_file_stream_writer.

On closer inspection, I think you'll probably want to put all 3 of those members in the struct.

See below for updated code.

So this in effect means that the 3 members in ofstream_holder now come before the flat_file_stream_writer in the layout of flat_file_writer.

This seems like the right solution, not just because it "squashes the compiler warning", but because now we are really getting the correct initialization order, ie construct the std::ofstream first and then pass a reference to it to flat_file_stream_writer. And during destruct the std::ofstream will be destroyed last.

The original code above had this exactly the wrong way around, because the order we write in the derived constructor initializer is ignored and the actual order implemented is layout order, ie the order the members are declared.

(despite the flaws of the original code, it "ran fine" for me with sanitizers etc... but was probably UB?).

template <typename ValueType>
class flat_file_stream_writer {

  public:
    explicit flat_file_stream_writer(std::ostream& os, std::size_t buf_size = 100)
        : db_(os), buf_(buf_size) {}

    void write(const ValueType& value) {
        if (buf_pos_ == buf_.size()) flush();
        std::memcpy(&buf_[buf_pos_], &value, sizeof(ValueType));
        ++buf_pos_;
    }

    void flush() {
        if (buf_pos_ != 0) {
            db_.write(reinterpret_cast<char*>(buf_.data()), // NOLINT reincast
                      static_cast<std::streamsize>(sizeof(ValueType) * buf_pos_));
            buf_pos_ = 0;
        }
    }

  private:
    std::ostream&          db_;
    std::size_t            buf_pos_ = 0;
    std::vector<ValueType> buf_;
};

struct ofstream_holder {
    explicit ofstream_holder(std::string dbfilename)
        : dbfilename_(std::move(dbfilename)), dbpath_(dbfilename_),
          ofstream_(dbpath_, std::ios::binary) {}

    std::string           dbfilename_;
    std::filesystem::path dbpath_;
    std::ofstream         ofstream_;
};

template <typename ValueType>
class flat_file_writer : private ofstream_holder, public flat_file_stream_writer<ValueType> {
  public:
    explicit flat_file_writer(std::string dbfilename)
            : ofstream_holder(std::move(dbfilename)), flat_file_stream_writer<ValueType>(ofstream_) {
        if (!ofstream_.is_open())
            throw std::domain_error("cannot open db: " + std::string(dbpath_));
    }
};



~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文