Два факта про неочевидные (и к счастью редкие) кеки C++.
Первое про union.
Важно помнить, что у
union в C++ могут быть конструкторы/деструкторы, потому что важно уметь выбирать кого вы конструируете по умолчанию, и соответственно уметь вызывать корректный деструктор в зависимости от активного члена. И вот если у вас объектам надо вызывать конструктор, и вы union’у его не реализовываете, то код просто не компилируется, потому что дефолтный по умолчанию удалён. Но это лирическое отступление. Имеем такой union:union U {
std::string s_;
std::vector<int> v_;
U(std::string s) { new (&s_) std::string(s); }
U(std::vector<int> v) { new (&v_) std::vector<int>(v); }
~U() {}
};И собственно сам кек. Т.к. по дефолту деструктор удалён, то в данном случае
= default для него означает, что деструктора нет. Т.е. поведение при пустом кастомном деструкторе и при ~U() = default отличается.Вы спросите, а почему в деструкторе ничего не делаем? Я отвечу: потому что этим должен заниматься пользователь
union снаружи. Он знает, какой член является активным. Он пусть с этим и разбирается. Ну и чтобы соблюсти формальности, уточним, что вызывать деструктор
std::string не как s_.~string()а как
s_.~basic_string<char>()потому что это же алиас.
Второе про function-try-block.
В C++ существует альтернативный синтаксис для определения тела функции, позволяющий навесить на него целиком перехват и обработку исключений:
// Стандартный способ
void f() {
try {
may_throw();
} catch (...) {
handle_error();
}
}
// Альтернативный синтаксис
void f() try {
may_throw();
} catch (...) {
handle_error();
}Эта фича позволяет нам ловить исключения из списка инициализации в конструкторах:
struct S {
A a_;
B b_;
S(A a, B b) try : a_(a), b_(b) {
} catch (…) {
// handle
}
};Правда тут есть один опасный момент. После обработки ошибки в
catch ошибка будет неявно проброшена выше. Это в целом логично: если при инициализации полей класса вылетело исключение, мы никак не можем исправить ситуацию и починить объект.А что с деструкторами? Из них же крайне нежелательно бросать исключения, потому словить всё было бы удобно. Но тут пранк — поведение такое же, как и в конструкторах. Т.е.
catch из function-try-block неявно пробрасывает исключение выше. Вы только что нарушили неявный noexcept(true). Но в отличие от конструкторов, для деструкторов есть мегакостыль: просто добавьте return!struct A {
~A() try {
throw std::runtime_error("err");
} catch (…) {
std::cout << “ERROR”;
return; // исключение не будет перевыброшено!
}
};Таким образом словили кейс, когда
return в конце void-функции меняет её поведение.На всякий напомню, что ловить исключения через
catch (…) не очень хорошо. Вообще можно почитать мою статью про обработку ошибок. Ну а плюсы что. Кринж.