Помимо неопределенного поведения, в C++ есть неожиданное поведение, произрастающее из следующих фантастических возможностей языка.
Пользовательские типы и функции можно объявлять где попало и как попало.
template <class T>
struct STagged {};
using S1 = STagged<struct Tag1>; // преобъявление струкруты Tag1
using S2 = STagged<struct Tag2*>; // преобъявление струкруты Tag2
void fun(struct Tag3*); // предобъявление структуры Tag3
void external_fun() {
int internal_fun(); // предобъявление функции!
internal_fun();
}
int internal_fun() { // определение предобъявленой функции
std::cout << "hello internal\n";
return 0;
}
int main() {
external_fun();
}
При этом определять сущности можно не везде. Типы можно определять локально — внутри функции. А функции определять нельзя.
void fun() {
struct LocalS {
int x, y;
}; // OK
void local_f() {
std::cout << "local_f";
} // Compilation Error
}
И все могло бы быть хорошо, если бы не одно: в C++ есть конструкторы, вызов которых похож на объявление функции
struct Timer {
int val;
explicit Timer(int v = 0) : val(v) {}
};
struct Worker {
int time_to_work;
explicit Worker(Timer t) : time_to_work(t.val) {}
friend std::ostream& operator << (std::ostream& os, const Worker& w) {
return os << "Time to work=" << w.time_to_work;
}
};
int main() {
// ЭТО НЕ ВЫЗОВ КОНСТРУКТОРА!
Worker w(Timer()); // предобъявление функции, которая возвращает Worker и принимает функцию, возвращающую Timer и не принимающую ничего!
std::cout << w; // имя функции неявно преобразуется к указателю, который неявно преобразуется к bool
// будет выведено 1 (true)
}
Подобная ошибка может быть труднообнаружима, если случайно предобъявленная функция используется в контексте приведения к bool
или если объект, который хотели сконструировать, сам является вызываемым (у него перегружен operator()
).
Может показаться, что виноват именно конструктор по умолчанию класса Timer
. На самом деле, виноват C++.
В нем можно объявлять функции вот так:
void fun(int (val)); // скобки вокруг имени параметра допустимы!
И потому получать более отвратительный и труднопонимаемый вариант ошибки:
int main() {
const int time_to_work = 10;
Worker w(Timer(time_to_work)); // предобъявление функции, которая возвращает Worker
// и принимает параметр типа Timer. time_to_work — имя этого параметра!
std::cout << w;
}
Clang способен предепреждать о подобном.
С++11 и новее предлагают universal initialization (через {}
), которая не совсем universal и имеет свои проблемы.
C++20 предлагает еще одну universal инициализацию, но уже снова через ()
...
Избежать проблемы можно, используя Almost Always Auto подход с инициализацией вида
auto w = Worker(Timer())
. Круглые или фигурные скобки здесь — не так важно (хотя, на самом деле, важно, но в другой ситуации).
Возможно, когда-нибудь объявление функций в старом сишном стиле запретят в пользу
trailing return type (auto fun(args) -> ret
). И вляпаться в рассмотренную прелесть станет значительно сложнее (но все равно можно!).