【问题标题】:How can `std::error_category` safely be used in static destructors?`std::error_category` 如何在静态析构函数中安全使用?
【发布时间】:2019-07-23 05:12:50
【问题描述】:

考虑使用LLVM system_category 实现编写的自定义错误类型以供参考:

#include <iostream>
#include <system_error>

struct my_error_category_type : std::error_category {
    char const* name() const  noexcept override { return "name"; }
    std::string message(int i) const noexcept override{ return "message"; }
    ~my_error_category_type() {
        std::cout << "Destroyed the category" << std::endl;
    }
};

std::error_category const& my_error_category() noexcept {
    static my_error_category_type c;
    return c;
}

现在想象以下使用std::error_code 处理错误的简单类:

std::error_code do_some_setup() {
    return std::error_code(1, my_error_category());
}
std::error_code do_some_cleanup() {
    return std::error_code(2, my_error_category());
}

struct MyObj {
    void method() {
        // this constructs the category for the first time
        auto err = do_some_setup();
        std::cout << err << std::endl;
    }

    ~MyObj() {
        std::cout << "Running cleanup" << std::endl;
        auto err = do_some_cleanup();
        std::cout << err << std::endl;
    }
};

以下代码给出报警输出

static MyObj obj;

int main() {
    obj.method();  // remove this line, and the output is fine
}
name:1
Destroyed the category
Running cleanup
name:2

注意my_error_category_type::message 是如何在被破坏的对象上调用的!

我的问题是:

  1. 在这个被破坏的对象上调用message 安全吗?
  2. 如果没有,有没有办法保留类别的生命周期?我能以某种方式使对象不朽吗?
  3. 标准是否对内置std::system_category() 对象等的生命周期做出任何保证?我在上面链接到的 LLVM 实现遇到了完全相同的问题。

【问题讨论】:

标签: c++ c++11 language-lawyer std-system-error


【解决方案1】:

在调用析构函数后调用对象方法是不安全的。

问题是如何控制对象的破坏顺序。静态对象的破坏顺序与初始化相反,因此 my_error_category_type 会在 MyObj 之前被破坏,因为它的构造函数是在 MyObj 构造函数之后调用的。这不是一个问题,需要通过标准来解决,而是架构问题。

所以,我们必须以某种方式控制破坏顺序。 最简单的方法是确保obj析构函数被更早地调用:

void F() {
  MyObj obj;
  obj.method(); 
}

int main() {
  F();
}

程序输出:

name:1
Running cleanup
name:2
Destroyed the category

现在MyObj 析构函数被更早地调用,不是在main 之后,而是在F() end 之后,因为MyObj 是一个作用域变量,它在F() 完成后被破坏,static my_error_category_type c 在 main 完成时被破坏。

但是,如果我们仍然想让 MyObj 成为静态,有一种称为 Nifty Counter Idiom 的技术,它有助于在最后一次使用后才破坏静态。但它有其权衡。 link

与静态类似的问题:“静态初始化顺序惨败”(link)。

【讨论】:

    【解决方案2】:

    不能在静态析构函数中安全地使用静态 std::error_category

    这正是"static initialization order fiasco"(ISO C++ 超级常见问题解答),但在销毁时,所以 - “静态销毁命令惨败”:您无法保证任何事情,或者至少您很难知道是否和关于静态持续时间对象被破坏的顺序,您可以得到什么保证。

    因此,您不能依赖此顺序,特别是静态存储持续时间对象(全局变量或局部范围静态变量)的析构函数不能假定任何其他静态对象,例如std::error_category,没有被破坏。

    @DanielZ 建议通过将示例中的变量之一设置为范围来规避问题 - 这显然会起作用。我建议反对尝试操纵或设计销毁顺序,即使这是可能的,因为这会导致代码脆弱和混乱。

    【讨论】:

      【解决方案3】:

      这看起来像是std::system_category 的实现中的一个错误。

      作为一种变通方法,MyObj 构造函数可以调用std::system_category / my_error_category,以便在构造MyObj 之前构造函数静态错误类别,因此只有在MyObj 被销毁后才被销毁:

      MyObj::MyObj() noexcept { static_cast<void>(my_error_category()); }
      

      【讨论】:

        猜你喜欢
        • 2019-09-20
        • 1970-01-01
        • 1970-01-01
        • 2011-05-20
        • 1970-01-01
        • 1970-01-01
        • 2010-09-05
        相关资源
        最近更新 更多