Область видимости объекта начинается сразу же после его объявления. В той же строчке. Поэтому в С++ очень легко сконструировать синтаксически корректное выражение, использующее еще не сконструированный объект.
// просто и прямолинейно
int x = x + 5; // UB
//--------------
// менее явно
const int max_v = 10;
void fun(int y) {
const int max_v = [&]{
// локальный max_v перекрывает глобальный max_v
return std::min(max_v, y);
}();
...
}
Конечно, такой код вряд ли кто-то будет писать целенаправлено.
Но он может возникать самопроизвольно при применении средств автоматического
рефакторинга. Локальный max_v
во втором примере мог изначально называться как-то по-другому. Применили автоматическое переименование и получили вместо некомпилирующегося кода, код с неопределенным поведением.
Причем в следующей версии никакой проблемы не возникает:
const int max_v = 10;
void fun(int y) {
const int max_v = [y]{
// тут виден только глобальный max_v
return std::min(max_v, y);
}();
...
}
Код, уходящий в область неопределенного поведения при добавлении лишь одного символа — все как мы любим.
Такой код синтаксически валиден и никто не собирается его запрещать. Более того, он еще и не всегда приводит к UB.
К UB приводит только использование с, грубо говоря, разыменованием ссылки на этот объект. Почему грубо? Потому что правила такие же, как и с разыменованием nullptr
— то есть довольно путанные, а не просто лишь «никогда нельзя — всегда UB». Хотя использование такой радикальной трактовки уберет вас от многих бед.
struct ExtremelyLongClassName {
using UnspeekableInternalType = size_t;
UnspeekableInternalType val;
static UnspeekableInternalType Default() { return 5;}
};
ExtremelyLongClassName x { x.Default() + 5 }; // Ok, well-defined
ExtremelyLongClassName y {
[] ()-> ExtremelyLongClassName::UnspeekableInternalType {
// сложные вычисления
return 1;
}()
};
ExtremelyLongClassName z {
[] ()-> decltype(z.Default()) { // Ok, well-defined
// сложные вычисления
return 1;
}()
};
Также эта фича может быть полезна в каких-то специфических случаях, в которых вам зачем-то нужен объект, ссылающийся сам на себя
struct Iface {
virtual ~Iface() = default;
virtual int method(int) const = 0;
};
struct Impl : Iface {
explicit Impl(const Iface* other_ = nullptr) : other(other_) {
};
int method(int x) const override {
if (x == 0) {
return 1;
}
if (other){
return x * other->method(x - 1);
}
return 0;
}
const Iface* other = nullptr;
};
int main() {
Impl impl {&impl};
std::cout << impl.method(5);
}
Точно таким же образом, но более запутанно, можно завязать объекты в узел, используя делегирующие конструкторы. Но об этом в отдельной заметке.
Избежать использования объекта при инициализации его же самого можно, следуя правилу AAA
(almost always auto):
Всегда, если это возможно, использовать запись auto x = T {....}
для объявления и инициализации переменных.
В такой записи использование объявляемой переменной внутри инициализирующего дает ошибку компиляции.