1

Consider a custom error type written using the LLVM system_category implementation for reference:

#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;
}

Now imagine the following simple class that uses std::error_code to handle errors:

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;
    }
};

The following code gives alarming output

static MyObj obj;

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

Note how my_error_category_type::message was called on a destructed object!

My questions are:

  1. Is calling message on this destructed object safe?
  2. If not, is there a way to preserve the lifetime of the category? Can I make the object immortal somehow?
  3. Does the standard make any guarantees about the lifetime of the builtin std::system_category() objects and the like? The LLVM implementation I link to above suffers exactly the same problem.
einpoklum
  • 118,144
  • 57
  • 340
  • 684
Eric
  • 95,302
  • 53
  • 242
  • 374
  • Looks like the answer to 1. [might be yes](https://stackoverflow.com/q/30310299/102441) – Eric Mar 01 '19 at 07:48

3 Answers3

3

It is not safe to call object methods after its destructor is called.

The question is how to control objects order of destruction. Statics are destructed in reverse order of initialization, so my_error_category_type will be destructed before MyObj, because its constructor is called after MyObj constructor. It is not a problem, that needs to be solved by standard, but rather architectural problem.

So, we gotta somehow control the destruction order. The simplest way is to ensure that obj destructor is called earlier:

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

int main() {
  F();
}

Program output:

name:1
Running cleanup
name:2
Destroyed the category

Now MyObj destructor is called earlier, not after main, but after F() end, because MyObj is a scope variable, and it's destructed after F() finish and static my_error_category_type c is destructed when main finishes.

But if we still wanna make MyObj static, there is such technique called Nifty Counter Idiom, that helps to destroy statics only after last use. But it has its tradeoffs. link

Similar problem with statics: "static initialization order fiasco" (link).

Daniel Z.
  • 389
  • 2
  • 10
0

A static std::error_category can't be used safely in static destructors.

This is exactly the "static initialization order fiasco" (ISO C++ Super FAQ), but at destruction, so - "static destruction order fiasco": You are not guaranteed anything, or at the very least it is difficult for you to know if and what you are guaranteed anything, about the order in which static-duration objects are destructed.

Consequently, you can't rely on this order, and specifically, the destructor of a static-storage-duration object (a global variable or a local-scope static variable) cannot assume that any other static object, such as an std::error_category, is not-destructed.

@DanielZ suggested circumventing the problem by making one of the variables in your example scoped - and that will obviously work. I would recommend against trying to manipulate or engineer the destruction order, even if that is possible, because this will result in brittle and confusing code.

einpoklum
  • 118,144
  • 57
  • 340
  • 684
-1

It looks like a bug in the implementation of std::system_category.

As a work-around, MyObj constructor can call std::system_category / my_error_category, so that the function static error category is constructed before MyObj is constructed and hence destroyed only after MyObj is destroyed:

MyObj::MyObj() noexcept { static_cast<void>(my_error_category()); }
Maxim Egorushkin
  • 131,725
  • 17
  • 180
  • 271