【问题标题】:Use union to defer member variable construction使用 union 来推迟成员变量的构造
【发布时间】:2020-11-22 08:41:25
【问题描述】:

我想将我的成员变量的构造推迟到构造函数的主体,我正在尝试使用联合来做到这一点。到目前为止,它实现了我想要的,但我想问有什么理由我不应该这样做?

例子:

#include <iostream>

struct A {
  A() {
    std::cout << "Construct A" << std::endl;
  }
  ~A() {
    std::cout << "Destruct A" << std::endl;
  }
};

struct B {
  A a;
};

template <typename T>
union U {
  char a{};
  T buffer;
  U() {}
  ~U() {
    buffer.~T();
  }
};

struct C {
  U<B> u;
  C() {
    try {
      new (&u.buffer) B();
    } catch (...) {
    }
  }
};

编辑:添加示例用法

【问题讨论】:

  • 我看到的最直接的问题是它令人困惑。我认为chars 的数组就是sizeof A 将是要走的路,然后在B 的构造函数主体中使用operator new。这也能达到你想要的吗?另外,我确定您将施工推迟到尸体有效之前的原因,但是您介意为我们这些后排的人分享它们吗?
  • 但是我不能使用像u.b 这样的东西来保证类型安全
  • U 的析构函数不应该做任何事情。如果您只是在u 被销毁之前以某种方式最终没有初始化u.buffer,那么这使得获得UB 变得非常容易。如果u.buffer 的破坏在C::~C() 中,我会感觉更好。例如。如果B::B() 像现在这样输入这段代码,我认为会有 UB。否则,我认为它可能没问题。
  • @UyHà 如果B::B() 抛出,那么C::C() 将捕获并忽略异常。但是,B 对象 u.buffer 不会开始它的生命周期,因为它不是构造的。到目前为止,这很好。现在,最终C 将被销毁。在C::~C() 被调用之后,u.~U() 将被调用(你无法阻止它!)。 u.~U() 会尝试调用u.buffer.~B(),但是没有buffer 可以销毁。那是UB。一种解决方法是U&lt;B&gt;::~U() 什么都不做,C::~C() 在需要时做u.buffer.~B()std::optional 等实用程序封装了该逻辑。
  • 不,它仍然是 UB。一旦构造函数体开始执行,所有的成员变量都被初始化了。如果构造函数因异常退出,这些成员仍将被销毁。这也调用了u.~U(),然后是u.buffer.~B(),如果你没有成功初始化u.buffer,这将是UB。在这种情况下,我认为@yao99 的答案是一个好主意:完全摆脱U,只需使用函数-try 来检测成员初始化程序中的故障。基本上,一旦你点击了C::C(){,你必须以某种方式初始化u.buffer 以避免UB,这可能很困难。

标签: c++ initialization union destructor


【解决方案1】:

如果您使用 C++17,std::optional 似乎是一个很好的方法。

#include <iostream>
#include <optional>
#include <stdexcept>

struct A {
    A(bool fail = false) {
        std::cout << "Attempting to construct A" << std::endl;
        if (fail) {
            throw std::runtime_error("Failed to construct A");
        }
        else {
            std::cout << "Succeeded in constructing A" << std::endl;
        }
    }

    ~A() {
        std::cout << "Destruct A" << std::endl;
    }
};

struct B {
    std::optional<A> a;

    B(bool fail = false) {
        try {
            a.emplace(fail);
        }
        catch (std::runtime_error& ex) {
            // fall back to a safe construction
            std::cout << "Falling back to safe A construction" << std::endl;
            a.emplace();
        }
    }
};

int main() {
    {
        B b_good; // should be fine
    }

    {
        B B_bad(true); // should catch the exception and fall back
    }
}

输出:

Attempting to construct A
Succeeded in constructing A
Destruct A
Attempting to construct A
Failed to construct A

放弃std::optional 大小的一个选项是让未分配的缓冲区,但(为了类型安全)通过引用访问它。

#include <iostream>
#include <optional>
#include <stdexcept>

struct A {
    A(bool fail = false) {
        std::cout << "Attempting to construct A" << std::endl;
        if (fail) {
            throw std::runtime_error("Failed to construct A");
        }
        else {
            std::cout << "Succeeded in constructing A" << std::endl;
        }
    }

    ~A() {
        std::cout << "Destruct A" << std::endl;
    }
};

struct B {
    char a_buff_[sizeof(A)];
    A& a_;

    B(bool fail = false) : a_(*reinterpret_cast<A*>(a_buff_)) {
        try {
            new (&a_) A(fail);
        }
        catch (std::runtime_error& ex) {
            std::cout << ex.what() << std::endl;
            std::cout << "Falling back to safe A construction" << std::endl;
            new (&a_) A();
        }
    }

    ~B() { a_.~A(); }

    B(const B& other) : a_(other.a_) {}

    B& operator=(const B& other) {
        a_ = other.a_;
    }
};

int main() {
    {
        B b_good; // should be fine
    }
    
    {
        B b_bad(true); // should catch the exception and fall back
    }
}
Attempting to construct A
Succeeded in constructing A
Destruct A
Attempting to construct A
Failed to construct A
Falling back to safe A construction
Attempting to construct A
Succeeded in constructing A
Destruct A

【讨论】:

  • 它没有达到我想要的效果,我希望在构造函数的主体内捕获异常并且对象的大小尽可能小。
  • 我尝试了另一个使用缓冲区和引用的选项。虽然在这一点上我不确定它是否比你的建议更简单。
  • 是的,它变得太复杂太快了。
【解决方案2】:

您不应该使用该解决方法的原因之一是它没有意义。 在孔构造函数上应用 try-catch 会很好。

struct C {
    A a;
    C() try {
    } catch (...) {
    }
};

【讨论】:

  • 我看不出它如何捕获A 抛出的异常,但 TIL try 可以这样使用。
  • 一般语法其实就像C() try : a() { } catch(...) { }a 的构造发生在try 下,并由catch 监视。这个答案只是省略了: a(),因为它是暗示的。它仍然以相同的方式运行。 Godbolt demo
  • 这是en.cppreference.com/w/cpp/language/throw 上的一个例子。
  • 推迟成员的构造确实是有意义的;但是对于这个特定目的(在构造过程中处理异常),使用您的解决方案要好得多。不过,基于联合的解决方案提供了更大的灵活性(例如:使用不同参数重试构造成员)
  • @DanielJour 当然,这是有道理的。但似乎OP不想问。也许应该编辑这个问题。
猜你喜欢
  • 2015-07-03
  • 2014-05-08
  • 2011-08-08
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多