Инициализация в С++ действительно безумна. Лучше начинать с Си

https://mikelui.io/2019/01/03/seriously-bonkers.html
  • Перевод
Недавно мне напомнили, почему я считаю плохой идеей давать новичкам C++. Это плохая идея, потому что в C++ реальный бардак — хотя и красивый, но извращённый, трагический и удивительный бардак. Несмотря на нынешнее состояние сообщества, эта статья не направлена против современного C++. Скорее она частично продолжает статью Саймона Брэнда «Инициализация в C++ безумна», а частично — это послание каждому студенту, который хочет начать своё образование, глядя в бездну.

Типичные возражения студентов, когда им говорят об изучении C:

  • «Кто-то его ещё использует?»
  • «Это глупо»
  • «Почему мы изучаем C?»
  • «Мы должны учить что-то лучшее, например, C++» (смех в зале)

Кажется, многие студенты думают, что изучение C не имеет особого значения (от автора: это не так) и вместо этого нужно начинать с C++. Давайте рассмотрим только одну из причин, почему это абсурдное предложение: создание грёбаной переменной. В оригинальной статье Саймон Брэнд предположил, что читатель уже знаком со странностями инициализации в версиях до C++11. Мы же здесь посмотрим на некоторые из них и пойдём немного дальше.

Позвольте для начала пояснить, что в этой статье моё личное мнение, а не официальная позиция университета Дрекселя, где я преподаю на кафедре электротехники и вычислительной техники. Мои лекции обычно входят в курс инженерной программы, а не информатики, то есть больше относятся к системному программированию и встраиваемым системам.

Краткое содержание в одной гифке


u/AlexAlabuzhev на Reddit умудрился пересказать всю эту статью в одной гифке. (Думаю, это оригинальная работа Тимура Думлера)


Я ничего не имею против C++, но там много всего, что вам не нужно на раннем этапе.

Вот и всё. Иди домой. Погуляй с собакой. Постирай бельё. Позвони маме и скажи, что ты её любишь. Попробуй новый рецепт. Здесь нечего читать, ребята. В самом деле, подумайте о том, насколько плохо инженеры (то есть я) умеют доносить свои мысли…

Всё, я уговаривал как мог!

Итак, ты ещё здесь? Настоящий солдат. Если бы я мог, я бы дал тебе медаль! И вкусное шоколадное молочко!

Теперь вернёмся к нашему обычному… программированию.

Инициализация в C


Вступление


Сначала рассмотрим инициализацию в C, потому что она похожа на C++ по соображениям совместимости. Это будет довольно быстро, потому что C такой скучный и простой (кхм). Эту инициализацию назубок заучивает каждый новичок, потому что в C она работает иначе, чем во многих новых статически типизированных языках. Там либо инициализация по умолчанию для приемлемых значений, либо выдаётся ошибка компиляции.

int main() {
    int i;
    printf("%d", i);
}

Любой нормальный программист на C знает, что это инициализирует i как неопределённое значение (для всех намерений и целей i не инициализирована). Обычно рекомендуется инициализировать переменные, когда они определены, например int i = 0;, и переменные всегда следует инициализировать перед использованием. Независимо от того, сколько раз повторять, кричать, орать мягко напоминать студентам об этом, остаются те, кто считает, что переменная по умолчанию инициализируется в 0.

Отлично, попробуем ещё один простой пример.

int i;

int main() {
    printf("%d", i);
}

Очевидно, это одно и то же? Мы понятия не имеем о значении i — она может быть любой.

Нет.

Поскольку у переменной есть статическая продолжительность хранения, она инициализируется в беззнаковый ноль. Вы спросите, почему? Потому что так сказано в стандарте. Аналогичное поведение у типов указателей, которые я даже не собираюсь рассматривать в этой статье.

Окей, посмотрим на структуры.

struct A {
    int i;
};

int main() {
    struct A a;
    printf("%d", a.i);
}

То же самое. a не инициализирована. Мы увидим предупреждение при компиляции.

$ gcc -Wuninitalized a.c
a.c: In function ‘main’:
a.c:9:5: warning: ‘a.i’ is used uninitialized in this function [-Wuninitialized]
     printf("%d\n", a.i);

В C можно инициализировать объект несколькими простыми способами. Например: 1) с помощью вспомогательной функции, 2) во время определения или 3) присвоить некое глобальное значение по умолчанию.

struct A {
    int i;
} const default_A = {0};

void init_A(struct A *ptr) {
    ptr->i = 0;
}

int main() {
    /* helper function */
    struct A a1;
    init_A(&a1);

    /* during definition;
     * Initialize each member, in order. 
     * Any other uninitialized members are implicitly
     * initialized as if they had static storage duration. */
    struct A a2 = {0};

    /* Error! (Well, technically) Initializer lists are 'non-empty' */
    /* struct A a3 = {}; */

    /* ...or use designated initializers if C99 or later */
    struct A a4 = {.i = 0};

    /* default value */
    struct A a5 = default_A;
}

Это практически всё, что нужно знать об инициализации в C, и этого достаточно, чтобы вызвать множество хитрых ошибок во многих студенческих проектах. И уж точно проблемы появятся, если считать, что по умолчанию всё инициализируется в 0.

Инициализация в C++


Акт 1. Наш герой начинает путь


Если вам не терпится узнать все ужасы чудеса C++, сначала изучите способы инициализации переменных. Здесь такое же поведение, как в C из предыдущего кода, но с некоторыми оговорками в правилах этого поведения. В тексте я буду выделять курсивом специфический жаргон C++, чтобы подчеркнуть те моменты, где я не просто произвольно называю вещи, а указывают на огромное количество новых… возможностей… в C++ по сравнению с C. Начнём с простого:

struct A {
    int i;
};

int main() {
    A a;
    std::cout << a.i << std::endl;
}

Здесь у С и C++ почти одинаковое поведение. В C просто создаётся объект типа A, значение которого может быть любым. В C++ a инициализирована по умолчанию, то есть для построения структуры используется конструктор по умолчанию. Поскольку A настолько тривиальна, у неё неявно определённый конструктор по умолчанию, который в этом случае ничего не делает. Неявно определенный конструктор по умолчанию «имеет точно такой же эффект», как:

struct A {
    A(){}
    int i;
}

Чтобы проверить наличие неинициализированного значения, смотрим на предупреждение во время компиляции. На момент написания этой статьи g++ 8.2.1 выдавал хорошие предупреждения, а clang++ 7.0.1 в этом случае ничего не выдавал (с установленным -Wuninitialized). Обратите внимание, что включена оптимизация для просмотра дополнительных примеров.

$ g++ -Wuninitalized -O2 a.cpp
a.cpp: In function ‘int main()’:
a.cpp:9:20: warning: ‘a.A::i’ is used uninitialized in this function [-Wuninitialized]
     std::cout << a.i << std::endl;

По сути именно этого мы ожидаем от C. Так как же инициализировать A::i?

Акт 2. Наш герой спотыкается


Наверное, можно применить те же способы, что и в С? В конце концов, C++ является надмножеством С, верно? (кхм)

struct A {
    int i;
};

int main() {
    A a = {.i = 0};
    std::cout << a.i << std::endl;
}

$ g++ -Wuninitialized -O2 -pedantic-errors a.cpp
a.cpp: In function ‘int main()’:
a.cpp:9:12: error: C++ designated initializers only available with -std=c++2a or -std=gnu++2a [-Wpedantic]
     A a = {.i = 0};

Вот вам и родственники. Явные инициализаторы не поддерживаются в C++ до C++20. Это стандарт C++, который планируется к выходу в 2020 году. Да, в C++ функцию реализуют через 21 год после того, как она появилась C. Обратите внимание, что я добавил -pedantic-errors для удаления поддержки нестандартных расширений gcc.

Что насчёт такого?

struct A {
    int i;
};

int main() {
    A a = {0};
    std::cout << a.i << std::endl;
}

$ g++ -Wuninitialized -O2 -pedantic-errors a.cpp
$

Ну хоть это работает. Мы также можем сделать A a = {}; с тем же эффектом, что и нулевая инициализация a.i. Это потому что A представляет собой агрегированный тип. Что это такое?

До C++11 агрегированный тип (по сути) является либо простым массивом в стиле C, либо структурой, которая выглядит как простая структура C. Ни спецификаторов доступа, ни базовых классов, ни пользовательских конструкторов, ни виртуальных функций. Агрегированный тип получает агрегированную инициализацию. Что это значит?

  1. Каждый объект класса инициализируется каждым элементом связного списка по порядку.
  2. Каждый объект без соответствующего связного списка элементов получит значение «инициализировано».

Отлично, что это значит? Если у объекта другой тип класса с пользовательским конструктором, будет вызван этот конструктор. Если объект является типом класса без пользовательского конструктора, как A, он будет рекурсивно инициализирован определённым значением. Если у нас встроенный объект, как int i, то он инициализируется нулём.

Урррррррааа! Наконец-то мы получили своего рода значение по умолчанию: ноль! Ух ты.

После C++11 ситуация выглядит иначе… вернёмся к этому позже.

Трудно запомнить и запутано? Обратите внимание, что у каждой версии C++ свой набор правил. Так и есть. Это чертовски запутано и никому не нравится. Эти правила обычно действуют, поэтому обычно система работает так, будто вы инициализируете элементы как ноль. Но на практике лучше явно всё инициализировать. Я не придираюсь к агрегированной инициализации, но мне не нравится необходимость пробираться сквозь дебри стандарта, чтобы точно узнать, что происходит во время инициализации.

Акт 3. Герой забрёл в пещеру


Что ж, инициализируем А методом C++ с конструкторами (торжественная музыка)! Можем назначить элементу i в структуре А начальное значение в пользовательском конструкторе по умолчанию:

struct A {
    A() : i(0) {}
    int i;
};

Это инициализирует i в списке инициализаторов членов. Более грязный способ — установить значение внутри тела конструктора:

struct A {
    A() { i = 0; }
    int i;
};

Поскольку тело конструктора может делать практически что угодно, лучше выделить инициализацию в список инициализаторов членов (технически часть тела конструктора).

В C++11 и более поздних версиях можно использовать дефолтные инициализаторы членов (серьёзно, по возможности просто используйте их).

struct A {
    int i = 0; // default member initializer, available in C++11 and later
};

Окей, теперь конструктор по умолчанию гарантирует, что i установлен в 0, когда любая структура A инициализируется по умолчанию. Наконец, если мы хотим разрешить пользователям A задать начальное значение i, можно для этого создать другой конструктор. Или смешать их вместе с аргументами по умолчанию:

struct A {
    A(int i = 0) : i(i) {}
    int i;
};

int main() {
    A a1;
    A a2(1);

    std::cout << a1.i << " " << a2.i << std::endl;
}

$ g++ -pedantic-errors -Wuninitialized -O2 a.cpp
$ ./a.out
0 1

Примечание. Нельзя написать A a(); для вызова конструктора по умолчанию, потому что он будет воспринят как объявление функции с именем a, которая не принимает аргументов и возвращает объект A. Почему? Потому что кто-то когда-то давно хотел разрешить объявления функций в блоках составных операторов, и теперь мы с этим застряли.

Отлично! Вот и всё. Миссия выполнена. Вы получили толчок и готовы продолжать приключения в мире C++, раздобыв полезное руководство по выживанию с инструкциями по инициализации переменных. Разворачиваемся и идём дальше!

Акт 4. Герой продолжает погружаться в темноту


Мы могли бы остановиться. Но, если мы хотим использовать современные возможности современного C++, то должны углубиться дальше. На самом деле моя версия g++ (8.2.1), по умолчанию использует gnu++1y, что эквивалентно C++14 с некоторыми дополнительными расширениями GNU. Более того, эта версия g++ также полностью поддерживает C++17. «Разве это имеет значение?» — можете вы спросить. Парень, надевай свои рыболовные сапоги и следуй за мной в самую гущу.

Во всех последних версиях, включая C++11, реализован этот новомодный способ инициализации объектов, который называется список инициализации. Чувствуете, как холодок пробежал по спине? Это также называется единообразной инициализацией. Есть несколько веских причин использовать этот синтаксис: см. здесь и здесь. Одна забавная цитата из FAQ:

Единообразная инициализация C++11 не является абсолютно единообразной, но это почти так.

Список инициализации применяется с фигурными скобками ({thing1, thing2, ...}, это называется braced-init-list) и выглядит следующим образом:

#include <iostream> 
struct A {
    int i;
};
int main() {
    A a1;      // default initialization -- as before
    A a2{};    // direct-list-initialization with empty list
    A a3 = {}; // copy-list-initialization with empty list
    std::cout << a1.i << " " << a2.i << " " << a3.i << std::endl;
}

$ g++ -std=c++11 -pedantic-errors -Wuninitialized -O2 a.cpp
a.cpp: In function ‘int main()’:
a.cpp:9:26: warning: ‘a1.A::i’ is used uninitialized in this function [-Wuninitialized]
     std::cout << a1.i << " " << a2.i << " " << a3.i « std::endl;

Эй, эй, вы это заметили? Остался неинициализированным только a1.i. Очевидно, что список инициализации работает иначе, чем просто вызов конструктора.

A a{}; производит то же поведение, что и A a = {};. В обоих случаях a инициализируется пустым списком braced-init-list. Кроме того, A a = {}; больше не называется агрегатной инициализацией — теперь это copy-list-initialization (вздыхает). Мы уже говорили, что A a; создаёт объект с неопределённым значением и вызывает конструктор по умолчанию.

В строках 7/8 происходит следующее (помните, что это после C++11):

  1. Список инициализации для A приводит ко второму пункту.
  2. Срабатывает агрегатная инициализация, поскольку A является агрегатным типом.
  3. Поскольку список пуст, все члены инициализируются пустыми списками.
    1. int i{} приводит к инициализации значения i, равного 0.

А если список не пуст?

int main() {
    A a1{0}; 
    A a2{{}};
    A a3{a1};
    std::cout << a1.i << " " << a2.i << " " << a3.i << std::endl;
}

$ g++ -std=c++11 -pedantic-errors -Wuninitialized -O2 a.cpp
$

a1.i инициализируется в 0, a2.i инициализируется пустым списком, а a3 — копия, построенная из a1. Вы ведь знаете, что такое конструктор копий, верно? Тогда вы знаете также о конструкторах перемещения, ссылках rvalue, а также передаваемых ссылках, pr-значениях, x-значениях, gl-значе… ладно, неважно.

К сожалению, в каждой версии с C++11 значение агрегата изменялось, хотя функционально до сих пор между агрегатами C++17 и C++20 нет никакой разницы. В зависимости от того, какая используется версия стандарта C++, что-то может быть или не быть агрегатом. Тренд в направлении либерализации. Например, публичные базовые классы в агрегатах разрешены начиная с C++17, что в свою очередь усложняет правила инициализации агрегатов. Всё замечательно!

Как себя чувствуете? Немного водички? Сжимаются кулаки? Может, сделаем перерыв, выйдем на улицу?

Акт 5. Прощай, здравый смысл


Что произойдет, если A не является агрегатным типом?

Вкратце, что такое агрегат:

  • массив или
  • структура/класс/объединение, где
    • нет приватных/защищённых членов
    • нет заявленных или предоставленных пользователем конструкторов
    • нет виртуальных функций
    • нет инициализаторов членов по умолчанию (в C++11, для последующих версий без разницы)
    • нет базовых классов (публичные базовые классы разрешены в C++17)
    • нет унаследованных конструкторов (using Base::Base;, в C++17)

Так что неагрегатный объект может быть таким:

#include <iostream>
struct A {
    A(){};
    int i;
};
int main() {
    A a{};
    std::cout << a.i << std::endl;
}

$ g++ -std=c++11 -pedantic-errors -Wuninitialized -O2 a.cpp
a.cpp: In function ‘int main()’:
a.cpp:8:20: warning: ‘a.A::i’ is used uninitialized in this function [-Wuninitialized]
     std::cout << a.i << std::endl;

Здесь у A есть предоставленный пользователем конструктор, поэтому инициализация списка работает иначе.

В строке 7 происходит следующее:

  1. Список инициализации для A приводит ко второму пункту.
  2. Не-агрегат с пустым braced-init-list вызывает инициализацию значения, идём к третьему пункту.
  3. Найден пользовательский конструктор, так что вызывается конструктор по умолчанию, который ничего не делает в этом случае, a.i не инициализируется.

Что такое конструктор, предоставленный пользователем?

struct A {
    A() = default;
};

Это не конструктор, предоставленный пользователем. Это как если вооще не объявлено никакого конструктора, а A является агрегатом.

struct A {
    A();
};
A::A() = default;

Вот это конструктор, предоставленный пользователем. Это словно мы написали A(){} в теле, где А не является агрегатом.

И угадайте что? В C++20 формулировка изменилась: теперь она требует, чтобы у агрегатов не было объявленных пользователем конструкторов :). Что это означает на практике? Я не уверен! Давайте продолжим.

Как насчет следующего:

#include <iostream>
class A {
    int i;
    friend int main();
};
int main() {
    A a{};
    std::cout << a.i << std::endl;
}

A — это класс, а не структура, поэтому i будет приватным, и нам пришлось установить main в качестве дружественной функции. Что делает А не агрегатом. Это просто обычный тип класса. Это значит, что a.i останется неинициализированным, верно?

$ g++ -std=c++11 -pedantic-errors -Wuninitialized -O2 a.cpp
$

Чёрт побери. И это тогда, когда мы вроде начали разбираться со всем этим. Оказывается, a.i инициализируется как 0, даже если не вызывает инициализацию агрегата:

  1. Инициализация списка для A, переходим к пункту 2.
  2. Неагрегат, тип класса с конструктором по умолчанию, и пустой список braced-init-list вызывают инициализацию значения, переходим к пункту 3.
  3. Предоставленный пользователем конструктор не найден, поэтому инициализируем объект как ноль, переходим к пункту 4.
  4. Вызов инициализации по умолчанию, если неявно определённый конструктор по умолчанию не тривиален (в данном случае условие не срабатывает и ничего не происходит).

Что если мы попробуем агрегатную инициализацию:

#include <iostream>
class A {
    int i;
    friend int main();
};
int main() {
    A a = {1};
    std::cout << a.i << std::endl;
}

$ g++ -std=c++11 -pedantic-errors -Wuninitialized -O2 a.cpp
a.cpp: In function ‘int main()’:
a.cpp:7:13: error: could not convert ‘{1}’ from ‘<brace-enclosed initializer list>’ to ‘A’
     A a = {1};

A не является агрегатом, поэтому происходит следующее:

  1. Инициализация списка для A, переходим к пункту 2.
  2. Поиск подходящего конструктора.
  3. Нет способа преобразовать 1 в A, компиляция завершается ошибкой.

В качестве бонуса озорной примерчик:

#include <iostream>
struct A {
    A(int i) : i(i) {}
    A() = default;
    int i;
};
int main() {
    A a{};
    std::cout << a.i << std::endl;
}

Здесь нет приватных переменных, как в предыдущем примере, но есть пользовательский конструктор, как в предпоследнем примере: таким образом, A не является агрегатом. Предоставленный пользователем конструктор исключает нулевую инициализацию, верно?

$ g++ -std=c++11 -pedantic-errors -Wuninitialized -O2 a.cpp
$

Нет! Разберёмся по пунктам:

  1. Инициализация списка для A, переходим к пункту 2.
  2. Неагрегат, тип класса с конструктором по умолчанию, и пустой список braced-init-list вызывают инициализацию значения, переходим к пункту 3.
  3. Не найден пользовательский конструктор по умолчанию (вот что я упустил выше), поэтому объект инициализируется как ноль, переходим к пункту 4.
  4. Вызов инициализации по умолчанию, если неявно определённый конструктор по умолчанию не тривиален (в данном случае условие не срабатывает и ничего не происходит).

Один последний пример:

#include <iostream>
struct A {
    A(){}
    int i;
};
struct B : public A {
    int j;
};
int main() {
    B b = {};
    std::cout << b.i << " " << b.j << std::endl;
}

$ g++ -std=c++11 -pedantic-errors -Wuninitialized -O2 a.cpp
a.cpp: In function ‘int main()’:
a.cpp:11:25: warning: ‘b.B::<anonymous>.A::i’ is used uninitialized in this function [-Wuninitialized]
     std::cout << b.i << " " << b.j << std::endl;

b.j инициализируется, а b.i нет. Что происходит в этом примере? Не знаю! Все базы b и члены здесь должны получить нулевую инициализацию. Я задал вопрос на Stack Overflow, и на момент публикации этого сообщения не получил твёрдого ответа, кроме возможной ошибки компилятора люди пришли к консенсусу, что здесь ошибка компилятора. Эти правила тонкие и сложные для всех. Для сравнения, статический анализатор clang (не обычный компилятор) вообще не предупреждает о неинициализированных значениях. Разбирайтесь сами.

...(тупо смотрит на вас) (взгляд превращается в вежливую улыбку) хорошо, давайте нырнём ещё глубже!

Акт 6. Бездна


В C++11 появилось нечто под названием std::initializer_list. У него собственный тип: очевидно, std::initializer_list<T>. Вы можете создать его с помощью braced-init-list. И кстати, braced-init-list для списка инициализации не имеет типа. Не путайте initializer_list со списком инициализации и braced-init-list! Все они имеют отношение к спискам инициализаторов членов и инициализаторам членов по умолчанию, так как помогают инициализировать нестатические элементы данных, но при этом сильно отличаются. Они связаны, но разные! Несложно, правда?

struct A {
    template <typename T>
    A(std::initializer_list<T>) {}
    int i;
};

int main() {
    A a1{0};
    A a2{1, 2, 3};
    A a3{"hey", "thanks", "for", "reading!"};
    std::cout << a1.i << a2.i << a3.i << std::endl;
}

$ g++ -std=c++17 -pedantic-errors -Wuninitialized -O2 a.cpp
a.cpp: In function ‘int main()’:
a.cpp:12:21: warning: ‘a1.A::i’ is used uninitialized in this function [-Wuninitialized]
     std::cout << a1.i << a2.i << a3.i << std::endl;
                     ^
a.cpp:12:29: warning: ‘a2.A::i’ is used uninitialized in this function [-Wuninitialized]
     std::cout << a1.i << a2.i << a3.i << std::endl;
                             ^
a.cpp:12:37: warning: ‘a3.A::i’ is used uninitialized in this function [-Wuninitialized]
     std::cout << a1.i << a2.i << a3.i << std::endl;

Окей. У A один шаблонный конструктор, который принимает std::initializer_list<T>. Каждый раз вызывается конструктор, предоставляемый пользователем, что ничего не делает, поэтому i остаётся неинициализированным. Тип T выводится в зависимости от элементов в списке, а новый конструктор создаётся в зависимости от типа.

  • Таким образом, в восьмой строке {0} выводится как std::initializer_list<int> с одним элементом 0.
  • В девятой строке {1, 2, 3} выводится как std::initializer_list<int> с тремя элементами.
  • В десятой строке список инициализации braced-init-list выводится как std::initializer_list<const char*> с четырьмя элементами.

Примечание: A a{} приведёт к ошибке, так как тип не может быть выведен. Например, нам нужно написать a{std::initializer_list<int> {}}. Или мы можем точно указать конструктор, как в A(std::initializer_list<int>){}.

std::initializer_list действует примерно как типичный контейнер STL, но только с тремя компонентными функциями: size, begin и end. Итераторы begin и end вы можете нормально разыменовать, увеличивать и сравнивать. Это полезно, когда требуется инициализировать объект списками разной длины:

#include <vector>
#include <string>
int main() {
    std::vector<int> v_1_int{5};
    std::vector<int> v_5_ints(5);
    std::vector<std::string> v_strs = {"neato!", "blammo!", "whammo!", "egh"};
}

У std::vector<T> есть конструктор, который принимает std::initializer_list<T>, поэтому мы можем легко инициализировать векторы, как показано выше.

Примечание. Вектор v_1_int создан из его конструктора, который берёт std::initializer_list<int< init с одним элементом 5.

Вектор v_5_ints создан из конструктора size_t count, который инициализирует вектор из (5) элементов и инициализирует их в значения (в данном случае все равны 0).

Оки–доки, последний пример:

#include <iostream>
struct A {
    A(std::initializer_list<int> l) : i(2) {}
    A(int i = 1) : i(i) {}
    int i;
};
int main() {
    A a1;
    A a2{};
    A a3(3);
    A a4 = {5};
    A a5{4, 3, 2};
    std::cout << a1.i << " "
              << a2.i << " "
              << a3.i << " "
              << a4.i << " "
              << a5.i << std::endl;
}

На первый взгляд, это не слишком сложно. У нас два конструктора: один принимает std::initializer_list<int>, а другой с аргументами по умолчанию принимает int. Прежде чем посмотреть на выдачу ниже, попробуйте сказать, каким будет значение для каждого i.

Подумали...? Посмотрим, что получится.

$ g++ -std=c++11 -pedantic-errors -Wuninitialized -O2 a.cpp
$ ./a.out
1 1 3 2 2

С a1 всё должно быть легко. Это простая инициализация по умолчанию, которая выбирает конструктор по умолчанию, используя его аргументы по умолчанию. a2 использует список инициализации с пустым списком. Поскольку у A есть конструктор по умолчанию (с аргументами по умолчанию), происходит инициализация значения с простым обращением к этому конструктору. Если бы у A не было этого конструктора, то пошло бы обращение к конструктору в третьей строке с вызовом пустого списка. a3 использует скобки, а не список braced-init-list, поэтому разрешение перегрузки выбирает 3 с конструктором, принимающим int. Далее, а4 использует список инициализации, для которого разрешение перегрузки склоняется в пользу конструктора, принимающего объект std::initializer_list. Очевидно, a5 нельзя соотнести с каким-то int, поэтому используется тот же конструктор, что и для a4.

Эпилог


Надеюсь, вы поняли, что эта статья (в основном) полемическая и, надеюсь, немного информативная. Многие описанные здесь нюансы можно игнорировать, и язык будет предсказуемо реагировать, если вы не забудете инициализировать переменные перед использованием и инициализировать элементы данных во время построения. Для написания грамотного кода необязательно изучать все пограничные ситуации С++, вы всё равно по ходу работы разберётесь с подводными камнями и идиомами. Для ясности, список инициализация — хорошая вещь. Если вы написали конструктор по умолчанию, он вызывается и должен всё инициализировать. В противном случае все инициализируется нулём, а затем независимо активируются дефолтные инициализаторы членов. Неинициализированное поведение тоже нужно оставить, потому что где-то, вероятно, есть код, который полагается на неинициализированные переменные.

Надеюсь, мне удалось продемонстрировать, что C++ большой, трудный язык (по многим историческим причинам). Вся статья посвящена нюансам инициализации. Просто инициализации переменных. И мы даже не раскрыли тему целиком, а кратко описали лишь 5 типов инициализации. Саймон в оригинальной статье упоминает 18 типов инициализации.

Я бы не хотел обучать новичков программированию на примере C++. В этой статье не нашлось места концепциям системного программирования, рассуждениям о парадигмах программирования, методологиям решения задач или фундаментальным алгоритмам. Если вы заинтересованы в C++, то записывайтесь на курс конкретно по C++, но имейте в виду, что там будут изучать именно этот язык. Если вам интересует C с классами или C с пространствами имён, то сначала узнайте о реализации this и коллизиях идентификаторов в C.

C — отличный, чёткий, быстрый, хорошо поддерживаемый и широко используемый язык для решения проблем в различных областях. И у него точно нет 18 типов инициализации.



Кстати, я совершенно забыл, что рассуждал точно на эту тему месяц назад. Вот что делает подсознание.



Обсуждение этой статьи и критика на разных форумах:

  1. Lobste.rs
  2. Hacker News
  3. Reddit

Отвечая на самую распространённую критику: да, можно научиться разумным способам инициализации переменных и никогда не встретиться с бездной. На этот счёт я специально написал в эпилоге, что список инициализации — хорошая вещь. Лично я редко пользуюсь шаблонами, но всё равно использую C++. Дело не в этом. Дело в том, что начинающий программист может полностью игнорировать STL и использовать стандартную библиотеку C, игнорировать ссылки, исключения и наследование. Так мы приближаемся к C с классами, за исключением того, что это не C, и вы всё ещё не понимаете указатели, выделение памяти, стек, кучу, виртуальную память. И теперь всякий раз, когда мне действительно нужен C, я должен переключиться на другой язык, который мог выучить с самого начала. Если вы собираетесь использовать C++, используйте C++. Но если вы хотите использовать C++ без всех особенностей C++, то просто изучите C. И повторю из первого абзаца, я не против C++. Мы видим бородавки на теле любимых и всё равно любим их.

И это всё, что я могу сказать об этом.
Поделиться публикацией

Комментарии 156

    +7
    Мда… И потом они ругают Python, PHP, Node за странности.
      +6
      Правильно ругают. C++ — язык с массой странностей, которые возникают из принципа «платите только за то, что используете», который требуется для написания быстрого и эффективного (по памяти) кода. Всё, что описано в статье, в общем, отсюда.

      Но с какого перепугу в языках, которые жрут память как не в себя и работают со скоростью черепахи куча закидонов?
        0
        Не нужно путать теплое с мягким. Речь не о потреблении памяти и скорости. Речь о странностях самого языка. Они есть везде фактически. Поэтому вместо того, чтобы искать козла отпущения, нужно заниматься исправлением последних в своем любимом языке.
          +6

          Отлично, то есть я (шарпист) могу продолжать ругать PHP и JS, спасибо!

            +7
            Мне кажется, исправить странности в C++ уже в принципе невозможно. Причина странностей — сохранение обратной совместимости при постоянном навешивании нового функционала. Это приводит к появлению нелогичных и многословных конструкций.
              +2
              Нет, в данном случае именно принцип «платите за то что используете». У разработчиков C++ были такие понятия что инициализация занимает время и жрет ресурсы. Зачем вам инициализация в 0 для всех целых чисел по умолчанию вы ведь наверняка собираетесь присвоить им какие-то другие значения?

              Например в Java, которую разрабочики тогда противопоставляли именно C++ сразу же был сделан противоположный выбор)))

              Инициализировано всегда и все)))

              Второй выбор, сделанный с той же целью, со сборкой мусора вместо ручного управления памятью более известен публике )
                +2
                Нет, в данном случае именно принцип «платите за то что используете». У разработчиков C++ были такие понятия что инициализация занимает время и жрет ресурсы. Зачем вам инициализация в 0 для всех целых чисел по умолчанию вы ведь наверняка собираетесь присвоить им какие-то другие значения?

                Нет, тут именно что навешено 100500 культурных слоев. Никто же не предлагает доработать алгол, чтобы писать на нем современные сайты? Язык решал задачи, язык стал прародителем многих современных, но писать на нем самом, пусть даже доработанном как угодно, наверное все же не стоит.


                Для примера, в некоторых современных языках есть три способа создать переменную: проициниализировать рекурсивно все поля руками, вызвать mem::unitialized() или mem::zeroed(). Вроде как покрывает все потребности.


                Не говоря про то, что даже эти способы деприкейтят в пользу MaybeUninit, потому что с ними легко отстрелить себе ногу. Инициализация без вызова какого-либо конструктора есть конструирование объекта в невалидном состоянии. То есть уб, по сути.

                  0
                  Никто же не предлагает доработать алгол, чтобы писать на нем современные сайты?
                  Вот именно поэтому алгол и его потомки и сошли со сцены. Почему в 80е все писали на Паскале (кто-то на СМ'ках, кто-то на PC, да даже и Mac — тоже изначально был на диалекте Паскаля написан), а уже в 90е — перестали? Во-многом — именно потому, что Модула-2/Оберон и так далее не позволяли использовать сущесвующий код, а C++ — позволял.

                  Не говоря про то, что даже эти способы деприкейтят в пользу MaybeUninit, потому что с ними легко отстрелить себе ногу. Инициализация без вызова какого-либо конструктора есть конструирование объекта в невалидном состоянии. То есть уб, по сути.
                  Ну вот и посмотрим как это всё произойдёт. Устроят тот же бардак, что Вирт с сотоварищами с потомками Алгола устроил — займут ту же нишу. Важных языков, про которые вы можете почитать в книжке «история Языков Программирования».

                  Нет, тут именно что навешено 100500 культурных слоев.
                  Увы, но иначе — никак. Либо у вас 100500 культурных слоёв, либо вы — странчика в википедии.
                    +1
                    Вот именно поэтому алгол и его потомки и сошли со сцены. Почему в 80е все писали на Паскале (кто-то на СМ'ках, кто-то на PC, да даже и Mac — тоже изначально был на диалекте Паскаля написан), а уже в 90е — перестали? Во-многом — именно потому, что Модула-2/Оберон и так далее не позволяли использовать сущесвующий код, а C++ — позволял.

                    Pascal->Delphi->C# живет и здравствует, однако. Кто куда сошел со сцены — непонятно…

                    Увы, но иначе — никак. Либо у вас 100500 культурных слоёв, либо вы — странчика в википедии.

                    Citation needed.

                    Вы так уверены, что через 1000 лет мы будем продолжать С++ пользоваться?
                      +2
                      Pascal->Delphi->C# живет и здравствует, однако. Кто куда сошел со сцены — непонятно…
                      Вот только не надо сову на глобус натягивать, ей больно. Классическая линейка это Algol→Pascal→Modula-2→Oberon→Oberon 2→забвение.

                      Delphi — это как раз попытка развития в духе «C/C++» (относительно успешная до момента, когда кто-то решил, что он умнее всех и сломал совместимость), но вот C# сюда вообще никаким боком не относится, это потомок совсем другого проекта (грубо говоря «Java с блекджеком и шлюхами», родившаяся из судебного решения). Ada — ещё чуть-чуть как-то где-то, но никак не C#, извините.

                      Вы так уверены, что через 1000 лет мы будем продолжать С++ пользоваться?
                      Нет, конечно. Время от времени замена происходила и будет проиходить. Но далеко не каждое десятилетие. Тот же C# за то время, которое потребовалось, чтобы потеснить C++ пережил массу изменений и в нём уже полно легаси — но если попытаться вот это вот всё выкинуть, то люди с него уйдут.

                      Да, со временем, если у вас много-много денег и терпения, лет за 10-15-20 — вы, может быть, и сможете новых разработчиков привлечь (как произошло с VB.NET, который вроде как все похоронили, списали со счетов… а он-таки начал набирать популярность лет 5 назад)… но только если вы будете эти 10-15-20 обеспечивать совместимость. Не будете, будете играть в те игры, что и Вирт… ну получите то, что получите.
                        0
                        Тот же C# за то время, которое потребовалось, чтобы потеснить C++ пережил массу изменений и в нём уже полно легаси — но если попытаться вот это вот всё выкинуть, то люди с него уйдут.

                        В отличие от C++, у C# (да и Java) есть огромное преимущество — компиляция в MSIL/байт-код и полная совместимость на уровне сборок. Это даёт возможность для развития многочисленных надстроек типа Kotlin, F# и т.д.

                          +2
                          и полная совместимость на уровне сборок.

                          У чего есть обратная сторона медали: те же генерики к Java прикручивали сбоку изолентой и получилось ну так себе.

                            0

                            Проблема в Java не столько в кривых дженериках, сколько в отсутствии типов-структур, что и повлияло на реализацию. И это камень в огород Sun, которая по каким-то причинам отказывалась их добавлять. Сейчас уже идут обсуждения о добавлении типов-структур в JVM и вполне возможно, в будущих версиях JVM это будет разрешено.


                            Кстати, в C#, когда в качестве дженерик-параметров используются только ссылочные типы, дженерики реализуются аналогично Java.


                            Та же Microsoft учла эти ошибки и реализовала абсолютно нормальный CIL ( https://stackoverflow.com/questions/95163/differences-between-msil-and-java-bytecode ). И он оказался настолько удачным, что я помню, чтобы его хоть как-то дорабатывали.

                              0
                              > Проблема в Java не столько в кривых дженериках, сколько в отсутствии типов-структур

                              Что-то вас мотает из стороны в сторону. В JVM ничего не ломали, поэтому при реализации генериков пришлось прибегнуть к type erasure.

                              > И он оказался настолько удачным, что я помню, чтобы его хоть как-то дорабатывали.

                              Только вот растущая популярность Go показывает, что наличие промежуточных представлений и тяжелого run-time — это недостаток для определенных ниш.
                                0
                                Что-то вас мотает из стороны в сторону. В JVM ничего не ломали, поэтому при реализации генериков пришлось прибегнуть к type erasure.

                                Нет. Я просто объяснил причину, почему дженерики в Java были реализованы именно как чисто языковая фишка без поддержки со стороны JVM.


                                Только вот растущая популярность Go показывает, что наличие промежуточных представлений и тяжелого run-time — это недостаток для определенных ниш.

                                И именно поэтому MS стала развивать отдельную ветку .NET Core.

                                  0
                                  > Я просто объяснил причину, почему дженерики в Java были реализованы именно как чисто языковая фишка без поддержки со стороны JVM.

                                  Да? И как бы наличие структур помогло бы избежать type erasure?

                                  > И именно поэтому MS стала развивать отдельную ветку .NET Core.

                                  Ну да, ну да.
                                    0
                                    Да? И как бы наличие структур помогло бы избежать type erasure?

                                    Наличие структур заставило бы искать другие пути решения проблемы. Потому что иначе пришлось бы либо мириться с боксингом структур в объекты.

                                      0
                                      Т.е. вы не знаете, как ваша идея-фикс помогла бы проблеме натягивания генериков на уже существующий байт-код JVM.
                                        0
                                        Наоборот, наличие структур сделало бы невозможным натагивание дженериков на существующий байт-код JVM, поэтому пришлось бы их делать нормально.
                                          –1
                                          На колу мочало, начинаем все сначала.

                                          Как наличие структур могло бы помощь избежать type erasure и, при этом, сохранить совместимость на уровне байт-кода?
                                            +1

                                            А в чём проблема совместимости на уровне байт-кода? Насколько я знаю, он уже несколько раз дорабатывался.

                                              0
                                              Как наличие структур могло бы помощь избежать type erasure и, при этом, сохранить совместимость на уровне байт-кода?

                                              Зачем сохранять прямую совместимость на уровне байт-кода? Достаточно просто расширить байт-код в очередной из версий JVM, не нарушив принципы обратной совместимости (работоспособность старых приложений в новых версиях среды). А о прямой совместимости (работа новых приложений в старых средах) речи никогда и не шло.


                                              Например, Microsoft в своё время так и сделала.

                                                0
                                                Зачем сохранять прямую совместимость на уровне байт-кода?

                                                Да без этого вот эти ваши слова:


                                                В отличие от C++, у C# (да и Java) есть огромное преимущество — компиляция в MSIL/байт-код и полная совместимость на уровне сборок.

                                                просто превращаются в тыкву. Поскольку если нет совместимости на уровне сборок или class-файлов, то о каком тогда преимуществе может идти речь?

                                                  +1
                                                  Как это нет? Обратная совместимость на уровне сборок есть и никуда не девалась. Код, написанный под старый фреймворк, будет работать и на новых фреймворках.
                                                    0

                                                    Соответственно, если в старом class-файле какой-нибудь метод ждет просто ArrayList, а из нового class-файла ему подсовывают ArrayList<MyType>, то что?

                                                      0
                                                      То же самое, что бывает когда старый метод ждет какой-нибудь Vector, а ему передают ArrayList.
                                                        0

                                                        То будет ошибка компиляции. Это два разных типа. Видимо, создатель старой библиотеки плохо подумал и вместо следования принципам SOLID вместо интерфейса List принимает на вход конкретный тип.


                                                        Например, в том же C# оба класса — и ArrayList и List<> реализуют не-дженерик интерфейс IList.

                                                          0

                                                          Ну а какая разница? Старый код ожидает List (который работает с Object-ом), а в новом уже List<T>.


                                                          На счет ошибки компиляции вообще хорошо. Вот написал кто-то Java-код на Java-1.4, отдал вам jar-файл. Вы захотели использовать его из Java-1.5 и...? Вам потребуются чужие исходники, чтобы пересобрать их в новый jar, но уже под Java-1.5?

                                                            0
                                                            Ну а какая разница? Старый код ожидает List (который работает с Object-ом), а в новом уже List<T>.

                                                            В C# сделали по-умному: дженерик-версия реализует и IList, и IList<T>.


                                                            Вы захотели использовать его из Java-1.5 и...?

                                                            И в худшем случае просто придётся написать враппер.

                                                              –2
                                                              В C# сделали по-умному: дженерик-версия реализует и IList, и IList<T>.

                                                              В разговоре про проблемы Java, которые возникли из-за стремления обеспечить максимальную совместимость, ссылаться на решения из .NET-а — это очень умно.


                                                              И в худшем случае просто придётся написать враппер.

                                                              Так ведь, насколько я помню, как раз из-за type erasure ничего писать и не приходилось.

                                                                +2
                                                                В разговоре про проблемы Java, которые возникли из-за стремления обеспечить максимальную совместимость, ссылаться на решения из .NET-а — это очень умно.

                                                                Вообще-то решение .NET как раз и обеспечивает максимальную совместимость.


                                                                В разговоре про проблемы Java, которые возникли из-за стремления обеспечить максимальную совместимость, ссылаться на решения из .NET-а — это очень умно.

                                                                Ага. Вот только сейчас Java почему-то навёрстывает упущенное и в ней появляется функционал, который уже давно есть в C#.

                                                                  0
                                                                  > Так ведь, насколько я помню, как раз из-за type erasure ничего писать и не приходилось.

                                                                  Сложно понять, почему этот комментарий собирает минусы. Но для тех, кто не застал переход с Java 1.4 на Java 1.5 и не в курсе того, что Java 1.5 с генериками позволяла осуществлять интероп с со старым Java-кодом (причем без перекомпиляции этого старого кода), может быть интересна вот эта ссылка: docs.oracle.com/javase/tutorial/extra/generics/legacy.html

                                                                  ЕМНИП, у Sun было несколько причин, по которым при реализации генериков было принято решение использовать type erasure. Одной из которых как раз была необходимость поддержать уже имеющееся легаси, причем без необходимости перекомпиляции старого кода новыми JDK.
                                                                    +1

                                                                    Код по вашей ссылке мог бы выглядеть вот так:


                                                                    public class Main {
                                                                        public static void main(String[] args) {
                                                                            Collection<Part> c = new ArrayList<Part>();
                                                                            c.add(new Guillotine()) ;
                                                                            c.add(new Blade());
                                                                            Inventory.addAssembly("thingee", c);
                                                                            Collection<Part> k = (Collection<Part>)Inventory.getAssembly("thingee").getParts();
                                                                        }
                                                                    }

                                                                    Но решили по-другому. В итоге выиграли всего-то отсутствие нескольких тайпкастов или копирований на стыке старого и нового кода — а проиграли целые автоматические контейнеры для примитивных типов...

                                                                      –3
                                                                      Да я уже и не знаю, как с отдельным персонажами разговаривать. Такая оторванность от жизненных реалий, что ППЦ.

                                                                      По поводу вашего комментария остается только сказать, что Sun потеряла очень многое из-за того, что не привлекла вас к реализации генериков в Java. Уж вы-то… Как же ж иначе.
                                +1
                                Delphi — это как раз попытка развития в духе «C/C++» (относительно успешная до момента, когда кто-то решил, что он умнее всех и сломал совместимость), но вот C# сюда вообще никаким боком не относится, это потомок совсем другого проекта (грубо говоря «Java с блекджеком и шлюхами», родившаяся из судебного решения). Ada — ещё чуть-чуть как-то где-то, но никак не C#, извините.

                                То-то я с делфи когда переходил, все было одно к одному. Синтаксис — да, сишный, а вот все остальное чувствуется. Рука чувствуется, знаете ли. Одна из причин, почему я именно начал учить шарп, а не джаву — очень похоже на дельфи было всё.

                                Нет, конечно. Время от времени замена происходила и будет проиходить. Но далеко не каждое десятилетие. Тот же C# за то время, которое потребовалось, чтобы потеснить C++ пережил массу изменений и в нём уже полно легаси — но если попытаться вот это вот всё выкинуть, то люди с него уйдут.

                                Окей, значит все же переломный момент где-то будет. Тогда вопрос: как определить, что этот момент настал? Почему вы так уверены, что 2019 это неподходящий год для того, чтобы выкинуть плюсы, а 3019 — подходящий?
                                  0

                                  Да, в Delphi (да и C++ Builder), в отличие от C++, уже появилось разделение на классы и интерфейсы.


                                  А ещё WinForms и визуальный редактор были подозрительно похожи на VCL. Собственно, благодаря именно этому я и перешёл с Delphi на C#, потому что все остальные средства создания GUI были ужасны.

                                    0
                                    Потому что «выкинуть» что-то можно не тогда, когда появляется альтернатива, а когда то, что вы используете перестаёт использоваться!

                                    В случае с C++ — это и близко тне так. И вообще я не удивлюсь если на какой-нибудь Rust люди перейдут не с C++, а, скажем, с C#.

                                    Тот же Go, скорее, привлёк любителей Java и Python, а не C++
                                      +3
                                      Потому что «выкинуть» что-то можно не тогда, когда появляется альтернатива, а когда то, что вы используете перестаёт использоваться!

                                      Ну так как оно перестанет использоваться, если альтернативами никто не пользуется. «Ведь столько на плюсах написано».
                                        0
                                        если альтернативами никто не пользуется

                                        Что, даже вы не пользуетесь?

                                    0
                                    Ну вообще-то Климент Шиперский, паскалист Виртовской школы и разработчик Black Box Component Builder сейчас тоже работает где-то в Microsoft. Другой вопрос в том, что C# сделали максимально похожим на C, убрали всё сходство с Algol60/Паскаль заодно и от виртовского минимализма избавились. Так что Шарп, он, конечно, наследник — но какой то незаконорожденный.

                                    А вообще Lua с некоторой оглядкой на Модулу делался, и это и в плане синтаксиса, и в плане минимализма как раз заметно. И в своей нише (встраиваемый в приложения скриптовый язык) Lua конечно испытывает серьёзную конкуренцию со стороны JS (унаследовавшего синтаксис C/C++) но помирать уж точно не собирается.
                                      0
                                      Дык эта. Никто ж не спорит, что идеи Алгола умерли — часть их много куда перекочевала.
                                0
                                Инициализация без вызова какого-либо конструктора есть конструирование объекта в невалидном состоянии. То есть уб, по сути.

                                Поэтому надо, значится, работать над системами типов, чтобы инвариант «используемый объект инициализирован» сохранялся, но без необходимости тратить такты на инициализацию нулём.


                                Ну или не надо, если на эти такты плевать.

                                  +1

                                  Ну дык, об этом я и говорю. Любой тип Т — инициализированный, инвариант сохраняется, MaybeUninit<T> — нет, и такты не тратятся.

                                    0

                                    То есть, это ADT? Ну тогда вы такты тратите на паттерн-матчинг.


                                    Я же говорю о чём-то субструктурно-подобном.

                                      +1
                                      ADT tagless, стирается в рантайме. Так что нет, не тратится.

                                      Для тех же причин, зачем есть NonZero.
                                        0

                                        Как это не тратится? Вот приходит мне в функцию MaybeUninit, откуда я знаю, что именно там лежит?

                                          +1
                                          Ниоткуда. Вы должны заранее знать что там лежит. Не случайно все функции читающие значение из MaybeUninit объявлены как unsafe.
                                            0

                                            Если вы читаете значение, то предполагается, что оно там есть. Чтение неиницализированного значения — UB.


                                            As you are probably discovering, mem::uninitialized makes it trivial to shoot yourself in the foot, I'd go as far as saying that it is almost impossible to use correctly. That's why we are trying to deprecate it in favor of MaybeUninit, which is a bit more verbose to use, but has the benefit that, because it is an union, you can initialize values "by parts" without actually materializing the value itself in an invalid state. The value only has to be fully valid by the time one calls into_inner().
                                              0
                                              Значит, не охраняет меня система типов от этого UB. Я говорю о том, чтобы компилятор вообще не дал мне этим объектом пользоваться, пока он не инициализирован.
                                                0
                                                Так в безопасном коде он и не даёт же.
                                                  0

                                                  Кажется, мы заходим на второй круг.


                                                  Как в безопасном коде он не даёт? Типа, создать неинициализированныйMaybeUninit — ансейф-операция, или как?

                                                    0
                                                    Нет, это безопасная операция. Но, оставаясь в рамках безопасного кода, вы не сможете ничего с этим MaybeUninit сделать, кроме как перезаписать его другим MaybeUninit.
                                                  0

                                                  Как выше сказали, вы не сможете достать значение без использования unsafe. А бездумное использование unsafe может приводить к UB, да.


                                                  На момент материализации значения оно должно быть валидным, и язык явно возлагает на разработчика ответственность, чтобы он не материализовывал объект в невалидном стейте.

                                                    –1
                                                    Ну, то есть, система типов этого не гарантирует.
                                                      0

                                                      Она валидирует на своем уровне.


                                                      Как вам система типов проверит что-то в FFI сценарии? Ведь MaybeUninit в основном используется именно для него. Выделить буфер, куда сишечка что-нибудь напишет, а потом прочитать ответ, вот это всё. Расскажете про язык, где этот вопрос решен на этапе компиляции без проверок?

                                                        0
                                                        Она валидирует на своем уровне.

                                                        Ну это так себе валидация, если после неё какие-то рантайм-UB могут быть.


                                                        Как вам система типов проверит что-то в FFI сценарии?

                                                        Никак, FFI — ансейф почти по определению. Но я говорю не об FFI, а о коде на самом языке.


                                                        Расскажете про язык, где этот вопрос решен на этапе компиляции без проверок?

                                                        Если бы я про такие языки знал, я бы написал «надо использовать $typesystemname системы типов», а не «надо работать над системами типов». Готовых решений нет (по крайней мере, мне они неизвестны), и эти решения тянут на пару хороших статей, если не на PhD.


                                                        То есть, нет, конечно, можно в очередной раз обмазаться зависимыми типами и в функции разворачивания этого MaybeUninit требовать доказательство, что объект инициализирован (которое компилятор может построить для тех объектов, про которые статически известно, что они инициализируются), и потом таскать его где надо, но императивные языки с полноценными завтипами мне тоже неизвестны. А в неимперативных такой проблемы нет, увы.

                                                          0
                                                          Она валидирует на своем уровне.
                                                          Ну это так себе валидация, если после неё какие-то рантайм-UB могут быть.
                                                          А никакой другой и нету. У какого-нибудь Хаскеля могут быть очень даже заметные UB, если в OS, в которой его запустят какой-нибудь mmap реализован с ошибками.

                                                          Rust гениален как раз тем, что когда мы не можем хорошо и грамотно выразить что-то в безопасном подмножестве — мы не обращаемся к другому, небезопасному языку, а прямо в том же, заводим отдельную, небезопасную функцию.

                                                          Возможность создания «безопасного языка» в котором всё, начиная с транзисторов верифицировано и доказано остаётся открытой — и его уж точно невозможно будет запускать на существующем железе.

                                                          Как вам система типов проверит что-то в FFI сценарии?
                                                          Никак, FFI — ансейф почти по определению. Но я говорю не об FFI, а о коде на самом языке.
                                                          А почему, извините? Почему, собственно, тот факт, что небезопасный код вы должны писать на другом языке и присоединять к безопасному коду через страшные прослойки — считается достоинством, а не недостатком?
                                                            0
                                                            У какого-нибудь Хаскеля могут быть очень даже заметные UB, если в OS, в которой его запустят какой-нибудь mmap реализован с ошибками.

                                                            Это неконструктивная гипотеза, так как в её рамках вообще ничего хорошего не может быть, и джаваскрипт от идриса неотличим.


                                                            Rust гениален как раз тем, что когда мы не можем хорошо и грамотно выразить что-то в безопасном подмножестве — мы не обращаемся к другому, небезопасному языку, а прямо в том же, заводим отдельную, небезопасную функцию.

                                                            Ансейф есть везде, вот, например, 35 строк адового ансейфа.


                                                            Почему, собственно, тот факт, что небезопасный код вы должны писать на другом языке и присоединять к безопасному коду через страшные прослойки — считается достоинством, а не недостатком?

                                                            У вас стрелка импликации повернулась, не надо так с ней.

                                                            0
                                                            Никак, FFI — ансейф почти по определению. Но я говорю не об FFI, а о коде на самом языке.

                                                            А в самом языке такие проблема пока есть, но будут решены. А MaybeUninit — это средство для FFI, ну и по совместительству костыль на то время, пока хорошие решения не стабилизированы.
                                                              0

                                                              Это тоже не совсем то, но уже существенно ближе.


                                                              А не то, например, вот поэтому, в части про conditional initialization:


                                                              Because if we do, then we can’t gaurentee that the value is in fact initialized.

                                                              Но, опять же, хороших решений для этого у меня пока нет.

                            +8
                            Есть неплохое выступление Мейерса, в котором он разбирает странности C++ www.youtube.com/watch?v=KAWA1DuvCnQ
                            Так вот он акцентирует внимание на том, что всё это не плохо, это не идиоты в комитете, у всего есть нормальное объяснение, почему именно так, а не иначе. Да, C++ сложен, но и задача, которую он перед собой ставит не тривиальна. Сложная задача не решается просто. Совсем отдельный вопрос, актуальна ли задача и нельзя ли было взять нишу поуже.
                              0
                                +7

                                Ну да, все верно, С и С++ разные языки, одинаковыми они кажутся только тем кто не знает ни того ни другого.
                                Хочу еще напомнить что


                                int x, y{};

                                значение x останется неопределенным, а y будет инициализован 0 и это сделано специально (неявный дефолтный конструктор не вызывается для встроенных типов) для тех редких случаев когда надо экономить каждый такт процессора. Это нужно исключительно редко, но те кому надо узнают и запоминают мгновенно, один раз услышав. Для остальных есть мнемоническое правило — инициализовать переменные при обьявлении, и что в этом плохого?
                                Такой вот язык С++, и мне он таким нравится.

                                  –10
                                  Если это не глобальная или статическая переменная, то «x» тоже будет 0 абсолютно на всех существующих компиляторах, совместимых с любым из стандартов. С нулевым количеством расходования тактов процессора.
                                    +5
                                    Строго наоборот — нулём окажется глобальная или статическая переменная. А вот в локальной будет «лежать» мусор.
                                      0
                                      Там так и напсиано. Это же ответ на предыдущий комментарий:
                                      — "… значение x останется неопределенным, а y будет инициализован 0..."
                                      -«Если это НЕ глобальная или статическая переменная, то...»
                                      В том плане, что предыдущий коментарий верен только если это НЕ глобальная или статическая переменная.
                                      Врыазился, конечно, наикривейше — факт.
                                      Интересно, что C++ уже 30 лет игнорирует то, что такие переменные имеют нулевые значения, не добавляя это в стандарт (хотя это не поломает никакой совместимости).
                                        0
                                        Интересно, что C++ уже 30 лет игнорирует то, что такие переменные имеют нулевые значения, не добавляя это в стандарт (хотя это не поломает никакой совместимости).

                                        Ну кто вам рассказал такую глупость? Написали простейший кусок кода типа hello world и "проверили"?


                                        Конечно, если стек изначально забит нулями, то первый раз в переменной будет ноль, но после пары вызовов функций там будет уже мусор. Ну или динамическая память — изначально она тоже забита нулями, пока менеджер динамической памяти не начнёт её переиспользовать.

                                          0
                                          Статические переменные живут не в стеке, а выделяемая для них память сразу заполняется нулями во всех (или почти во всех) операционных системах при загрузке программы в память.
                                            +1
                                            Память заполняется нулями во всех средах, соответствующих стандарту — о чём там написано. Составить пример, когда неинициализированная переменна на стеке будет содержать мусор — тоже несложно. Так что о чём пишет Sap_ru — остаётся только догадываться.
                                          +1
                                          Интересно, что C++ уже 30 лет игнорирует то, что такие переменные имеют нулевые значения, не добавляя это в стандарт (хотя это не поломает никакой совместимости).
                                          Это такой тонкий троллинг? Про это написано во всех стандартах начиная с C90! Вот как написали в стандарте If an object that ha> automatic storage duration is not initialized explicitly. its value is indetelminate. If an object that ha\ static storage duration is not initialized explicitly. it is initialized implicitly as if every member that has arithmetic type were assigned 0 and every member that has pointer type were assigned a null pointer constant. в самом первом стандарте — так это и кочует из стандарта в стандарт.
                                    +2

                                    Никогда не задумывался над этим огромным полем из граблей, всегда ходил по тропинкам, где их нет :-). Очень интересно было.


                                    Как в последнем примере: я не напишу так, даже если буду точно знать, где какой конструктор сработает. "Пиши код так, будто поддерживать его будет склонный к насилию психопат, знающий, где ты живёшь".

                                      –1

                                      В современных плюсах вроде модно так писать:


                                      auto a = A{};

                                      Половина проблем уже ушла, ну а если свой конструктор пишем то понятно что там надо все самим инициализировать правильно.


                                      А вот на си писать студентам не надо. Без RAII код замусорен всякими new, delete, close(). К тому же размер массива и данные хранятся в двух разных переменных. С таким излишними сложностями об алгоритмах некогда поговорить.

                                        +6

                                        Вы таки, видимо, не умеете си? Откуда в си new, delete? Эти операторы есть только в плюсах...


                                        Про размер массива тоже не очевидное утверждение. Кто мешает использовать структуру, если очень хочется? А во многих случаях со статическими массивами справляется sizeof.

                                          0
                                          Ну а в чем проблема то? =))
                                          #define new malloc
                                          #define delete free
                                            0

                                            Перепутал с malloc и free. Но смысл тот же, без std::vector не удобно.

                                              0

                                              Sizeof это вообще те ещё грабли, смотрите на статьи от единорога. Вы видимо хорошо знаете си если у вас это не вызывает сложностей. Зачем если есть std::array?

                                              +3
                                              Какой-то говнокод, как по мне, зачем там вообще auto?
                                              A a{};
                                              auto годится только для всяких мутных типов которые совсем лень писать вроде итераторов и прочих map::value_type с которыми легко ошибиться при ручном написании.
                                                +2
                                                auto годится только для всяких мутных типов
                                                А зачем вообще нужно дублировать имя типа там где он указывается явно? Не вижу вообще смысла не использовать auto кроме как для простых типов данных, случаев, когда тип переменной расходится с типом присваиваемого значения, и возможно еще каких исключений.
                                                  0
                                                  Я раньше был большим поклонником использования auto, но сейчас прихожу к мысли что все хорошо в меру.
                                                  Если в коде тип действительно вообще никак не влияет на понимание кода, если это какой-то там vector<string>, то auto может быть уместно. Если я только что сделал make_unique, то тоже тип и так очевиден. Во многих местах видеть тип реально сокращает время чтения кода. Многие пишут мол IDE, и так все видит, выводит типы, но я читаю код очень быстро, проматывая по странице в 2-3 секунды, и мне такие визуальные зацепки очень помогают.
                                                  Если нужно убрать когнитивную сложность из кода, скрыв «мусорный» тип, то auto вполне в тему)
                                                    +1

                                                    Да это те же люди, которые были против var в C# и других языках. Инерция мышления, однако.


                                                    Хаскеллисты и компания всю жизнь пишут let на всё подряд и никогда не страдают от того, что где-то чего-то непонятно.

                                                      0
                                                      Хаскеллисты и компания всю жизнь пишут let на всё подряд и никогда не страдают от того, что где-то чего-то непонятно.

                                                      Не пишу let (пишу where).


                                                      А если серьёзно, топ-левел-функции тоже никто не запрещает не аннотировать, это ж вам не идрис, и система типов классического хаскеля имеет достаточно мало undecidable мест в type inference (если вообще имеет, голый Haskell98/2010 скучный, я в его свойствах не шарю), чтобы аннотации не писать вообще никогда. Однако ж плохой тон.


                                                      Впрочем, я ближе к сторонникам auto, чем к противникам. Писать auto foo = double { meh }; я вряд ли буду, а вот auto foo = someObj.getFoo(); — почему б и нет?

                                                        0

                                                        В хачкеле емнип только топ декларации пишут, а отдельные личности и их опускают)


                                                        А я скорее про сторонников Person person = new Person()

                                                        0
                                                        В Хаскеле просто охренительный вывод типов + система типов гораздо более приспособлена для этого, чем в C++ (нет неявного приведения типов).
                                                      0

                                                      Как по мне, это абсолютно нормально. Например, могут быть случаи, когда нужно вызвать именно конструктор по умолчанию, а не передать список инициализации. Тогда вы можете написать так:


                                                      auto a = A();

                                                      Но написать так:


                                                      A a();

                                                      вы не сможете, придётся писать:


                                                      A a = A();

                                                      А дублировать название типа с обеих сторон (а оно может быть длинным) — это не очень хорошая практика.

                                                        –2
                                                        А раскрыть глаза и посмотреть на то, что вам предлагают нельзя? Написано
                                                        A a();
                                                        действительно нельзя, а вот
                                                        A a{};
                                                        написать можно всегда.

                                                        Может стоит уже начать забывать про C++98? Прошлый век, всё-таки…
                                                          +3
                                                          А раскрыть глаза и посмотреть на то, что вам предлагают

                                                          А предлагают фигню. Я хочу меньше думать над нюансами языка и больше — над задачей. Я реально не хочу думать над тем, где в случае использования фигурных скобок будет вызываться обычный конструктор, а где — конструктор, принимающий на вход std:initializer_list. Поэтому писать {} в качестве исключения для вызова конструктора по умолчанию меня несколько коробит.

                                                            0
                                                            А предлагают фигню.

                                                            Предлагают вещи, которые облегчают жизнь. Вряд ли кто-то в C++ до 11-го стандарта не наступал на грабли A a(). Сейчас про них вообще можно забыть, если использовать {}.


                                                            Кроме того, {} позволяют вам инициализировать POD-структуры:


                                                            struct S { int a_; int b_; int c_; };
                                                            
                                                            int main() {
                                                                S s{ 0, 2, 6 };
                                                            }

                                                            Кроме того, {} защищают вас от неявных усечений при инициализации:


                                                            int main() {
                                                                char a = 13045; // Предупреждение.
                                                                char a2{13045}; // Ошибка компиляции.
                                                                char a3(13045); // Предупреждение.
                                                            }

                                                            (цинк)


                                                            Так что инициализация через {} в современном C++ лучше в большинстве случаев, чем использование () из старого C++.


                                                            Да, при этом вылезли косяки с контейнерами, для которых есть конструкторы с initializer_list, поэтому std::vector<int>{10, 0} оказался не эквивалентен std::vector<int>(10, 0). Но это очередная ошибка эволюции. Бывает.

                                                              –2
                                                              Вряд ли кто-то в C++ до 11-го стандарта не наступал на грабли A a()

                                                              Я не наступал. Просто имел привычку либо объявлять конструктор по умолчанию, в котором инициализировал поля нулями, тогда можно было спокойно писать A a, либо инициализировал поля руками.


                                                              Предлагают вещи, которые облегчают жизнь.

                                                              Что реально добавило удобств, так это copy elision.


                                                              Кроме того, {} позволяют вам инициализировать POD-структуры:

                                                              Оно и раньше позволяло так делать.


                                                              Да, при этом вылезли косяки с контейнерами, для которых есть конструкторы с initializer_list ...

                                                              Поправили одно — вылезло другое. Не заглядывая в описание класса, невозможно понять, что перед нами: параметры конструктора или список инициализации. Как по мне, так косяк довольно серьёзный, и подобный функционал нужно использовать с максимальной осторожностью. Ведь есть же абсолютно логичное решение: std::vector<int>{{10, 0}}.

                                                                +2
                                                                Я не наступал.

                                                                Может вы просто шаблоны не писали. А то ведь:


                                                                template<class T> void demo() {
                                                                  T t; // Как здесь получить дефолтную инициализацию?
                                                                  ...
                                                                }

                                                                Что, если T — это int?


                                                                Оно и раньше позволяло так делать.

                                                                Такое ощущение, что вы в другое время жили. Ибо, вот что говорит gcc-4.7 (древнее у меня нет) с ключиком -std=c++98:


                                                                t1.cpp: In function 'int main()':
                                                                t1.cpp:4:7: warning: extended initializer lists only available with -std=c++11 or -std=gnu++11 [enabled by default]

                                                                А вот что говорит не менее древний VC++ с версией 17.00 (это вроде 2012-я студия):


                                                                t1.cpp(4) : error C2601: 's' : local function definitions are illegal
                                                                        t1.cpp(3): this line contains a '{' which has not yet been matched
                                                                t1.cpp(4) : error C2143: syntax error : missing ';' before '}'

                                                                Как по мне, так косяк довольно серьёзный

                                                                А кто-то спорит? Да, косяк оказался серьезным. Но не ошибается лишь тот, кто ничего не делает.


                                                                Ведь есть же абсолютно логичное решение

                                                                Задним умом все сильны.

                                                                  0
                                                                  Что, если T — это int?

                                                                  Не было такого на практике.

                                                                    0
                                                                    Ну а если бы пришлось, то что?
                                                                      0

                                                                      Тогда я бы написал так:


                                                                      T a = T();
                                                                        0

                                                                        Ну вот а кто-то писал вот так: T t(). И наступал на грабли. Которые теперь убраны.


                                                                        ЕМНИП, в свое время были споры, сколько реально создается объектов при записи T a=T().

                                                                          0
                                                                          ЕМНИП, в свое время были споры, сколько реально создается объектов при записи T a=T().
                                                                          До C++17 компилятор имеет право выбирать. Хотя в большинстве существующих компиляторов — всё-таки один. Хотя если нет конструктора копирования — то это ошибка компиляции.

                                                                          В C++17 гарантированно один — но это уже тонкости того же порядка, что и использованием гораздо более простой конструкции A a{};
                                                                            0
                                                                            > До C++17 компилятор имеет право выбирать.

                                                                            По-моему, еще в первом издании «Язык программирования C++» говорилось, что в этом случае будет конструироваться всего один объект. Но это было еще до принятия стандарта, как там это описывалось в C++98 я уже и не помню (а может и не знал).
                                                                              0
                                                                              До C++14 включительно считается, что конструируется два объекта (один с конструктором по умолчанию, потом его копия), но компилятор имеет право убирать конструкторы копирования, если единственным способом заметить его исчезновение — по его побочным эффектам.
                                                                          0
                                                                          А ничего, что эти записи не эквивалентны? И ваша конструкция не работает с объектами, которые нельзя копировать?
                                                                        0

                                                                        Контейнеры никогда не писали?

                                                                          0

                                                                          Писал, но у меня никогда не было необходимости инициализировать значения изнутри.

                                                            +1

                                                            Это не я придумал, было в одном из постов Herb Sutter. Ну а что зато похоже на rust:


                                                            let a = A::new();
                                                              +1

                                                              Такая конструкция не лишена логики. Лишнее ключевое слово даже при явном указании типа значительно упрощает парсер и читаемость кода.

                                                              +2
                                                              У конструкции
                                                              auto a = A{}
                                                              есть большой плюс в сравнении с
                                                              A a{};
                                                              — в ней невозможно забыть проинициализировать переменную. Компилятор обязательно выругается, если вы напишете auto a;, но молча пропустит A a;.
                                                            +8
                                                            Если учить студентов сначала Си, а потом Си++, то в итоге они пишут на Си++ код, состоящий из ручного управления памятью, константных указателей на указатели на short int, голых массивов и глобальных переменных.
                                                              +1
                                                              Ну так а решение-то какое? Если учить в обратном порядке, получается Сишный код с кучей утечек, дурацких memset которые зануляют только первые 4 байта и т.д.

                                                              По-моему в обоих случаях преподаватель должен четко давать понять что мы учим новый язык с нуля.
                                                                +4
                                                                Решение: сначала начинать с STL, потом уже переходить с базовым Сишным вещам. Пусть уж лучше пихают vector и shared_ptr куда не попадя, чем пишут программы, результат которых зависит от модели процессора (реальный случай в моей практике).
                                                                Вчерашние студенты не пишут код, где критична производительность. Надо будет — разберутся. Я считаю, что жыр ради стабильности — нормальный размен.
                                                                +3
                                                                Все равно что сказать что выучив основы одного единственного языка, все остальные языки и парадигмы становятся недоступными.
                                                                0
                                                                ИМХО. все написанное следует воспринимать не как «смотрите как сложно объяснять студентам правила инициализации в C++», а как «вот по каким правилам компилятор будет действовать, если вы, неучи, забудете сделать проинициализировать свои значения». С вполне однозначным выводом, который до народа пытаются донести с начала 1990-х (если не раньше): всегда инициализируйте переменные и члены класса. Всегда. Если только вам не нужно экономить каждый такт.
                                                                  0

                                                                  К сожалению, кроме написания своего кода ещё иногда приходится читать (а ещё хуже — использовать) чужой.

                                                                  0

                                                                  А мне не нравится инициализация в описании класса при наличии конструктора.
                                                                  Ходи потом по файлам и разбирайся какое значение должно быть.

                                                                    0
                                                                    Меня еще немножко бесит спецификатор const.
                                                                    Справа, слева, внутри…
                                                                      0
                                                                      Гораздо хуже constexpr. В случае с переменным — понятно зачем он нужен, а вот в случае с функциями… лишняя сущность, на самом деле, только чтобы разработчиков мучить.
                                                                        0
                                                                        Тогда вам от осознания того, что теперь еще добавился consteval, вообще плохо станет.
                                                                          0
                                                                          Вот как раз consteval накладывает на функцию ограничения и чего-то, всё-таки, значит. А constexpr - эта такая пометка "если надо - ну вычисли это в компайл-тайм, а если не очень или не получается... то и чёрт с ним - будет обычный вызов"...

                                                                          Ну и нафига это нужно? "Попробовать и, если не получилось, ругнуться" - компилятор мог бы и с любой обычной инлайн-функцией.
                                                                          0
                                                                          а вот в случае с функциями… лишняя сущность

                                                                          Ну а вот если в compile-time нужно размерность для std::array подсчитать в зависимости от нескольких параметров, то без constexpr-функций варианта всего два:


                                                                          • либо вычисления прямо по месту объявления std::array вписывать (дублирование кода);
                                                                          • либо использовать #define, в котором своих граблей полно.

                                                                          И как-то оба эти варианта не фонтан.


                                                                          И это не углубляясь в тему построения perfect-hash-ей в compile-time, контроля валидности форматных строк (как в свежих fmtlib), построения парсеров для регулярок и пр.

                                                                            +3
                                                                            Вы путаете возможность что-то посчитать в компайл-тайм и объявление функции constexpr.

                                                                            Считать что-то в компайл-тайм — удобно и полезно. Объявлять функции как constexpr — интеллектуальная мастурбация. Ибо в constexpr-функции разрешено, фактически, всё, что угодно. Вы можете создавать объекты и кидать исключения, читать из файла и писать в него, вызывать обычные (не inline и не constexpr функции) то есть делать вообще что угодно. И, если всё это безобразие происходит не там где нужна константа — программа будет валидной!

                                                                            Ну и кому, нафиг, всё это нужно? Если чтобы понять — будет что-то вычислено во время компиляции или нет всё равно нужно детективом работать?

                                                                            Если бы constexpr значит то, что в C++ обозначает consteval — вопросов бы не было.
                                                                              0
                                                                              Вы путаете возможность что-то посчитать в компайл-тайм и объявление функции constexpr.

                                                                              И на каком основании вы делаете такой вывод?


                                                                              Вот чего я не понимаю, так это вашего возмущения наличием constexpr для функций. Поясню причину моего непонимания.


                                                                              В C++98 для того, чтобы что-то подсчитать в компайл-тайме, мне нужно было либо использовать #define, либо погружаться в TMP, либо вообще использовать внешнюю кодогенерацию.


                                                                              В C++11 мне дали constexpr, а потом constexpr улучшили. Теперь я могу делать многое из того, что мне нужно было раньше гораздо более простыми средствами. Что, как по мне, просто очень хорошо.


                                                                              Цена за это — специальная отметка constexpr на тех функциях, которые могут быть вызваны в compile-time. Как по мне, как цена вполне приемлемая. Вон, в OCaml, рекурсивные функции нужно let rec помечать и ничего: выглядит коряво, зато работает.


                                                                              Так что мне решительно непонятно, что вас так в существовании constexpr-функций возмущает. И ваш эмоциональный комментарий ничего не прояснил, к сожалению.

                                                                                0
                                                                                Вы путаете возможность что-то посчитать в компайл-тайм и объявление функции constexpr.
                                                                                И на каком основании вы делаете такой вывод?
                                                                                На основании того, что вы тут рассказваете про C++98, про то, как круто, когда функции можно вызывать в рантайме и всё такое прочее. Но совершенно не хотите понять того, что всё это не требует модификатора constexpr на функции.

                                                                                Более того — то, что какая-то функция его имеет вам не даёт ровным счётом ничего — вы не знаете можно эту функцию использовать, чтобы «размерность для std::array подсчитать в зависимости от нескольких параметров»!

                                                                                Вон, в OCaml, рекурсивные функции нужно let rec помечать и ничего: выглядит коряво, зато работает.
                                                                                В Ocaml работает, а в C++ — нет.

                                                                                Так что мне решительно непонятно, что вас так в существовании constexpr-функций возмущает.
                                                                                Меня возмущает, что вот это вот:

                                                                                #include <iostream>
                                                                                
                                                                                constexpr void this_is_stupid(bool stupid_arg = true) {
                                                                                    if (stupid_arg) {
                                                                                      std::cout << "WTH is this?" << std::endl;
                                                                                    }
                                                                                }
                                                                                
                                                                                int main() {
                                                                                    this_is_stupid();
                                                                                }
                                                                                это, блин, корректная программа.

                                                                                Вот нафига нужна пометка, которая ни фига не гарантирует? Компилятор мог бы и так проверять inline функции на вычислимость в компайл-тайм и позволять их использовать там, где нам нужна константа.

                                                                                Вот consteval — это, чем constexpr должен был быть изначально. Но… сделали как сделали…
                                                                                  0
                                                                                  Вот нафига нужна пометка, которая ни фига не гарантирует? Компилятор мог бы и так проверять inline функции на вычислимость в компайл-тайм и позволять их использовать там, где нам нужна константа.

                                                                                  Отлично. У вас нет constexpr. Вы объявили константу. Как теперь понять, она вычислится при компиляции или в процессе работы программы?

                                                                                  0
                                                                                  На основании того, что вы тут рассказваете про C++98, про то, как круто, когда функции можно вызывать в рантайме и всё такое прочее.

                                                                                  Давайте вы будете следить a) за своей речью и b) за тем, что вам говорят оппоненты.


                                                                                  Я ничего не говорил про то, "как круто, когда функции можно вызывать в рантайме". Речь шла про то, что вычисления в компайл-тайм я теперь могу записывать в почти обычных функциях.


                                                                                  Но совершенно не хотите понять того, что всё это не требует модификатора constexpr на функции.

                                                                                  Еще раз повторю вопрос: почему вы решили, что я не могут понять, что модификатор constexpr избыточен?


                                                                                  Я вполне себе могу представить, что если функция объявлена как inline и компилятор заглянет в ее код, то может решить, способен он ее вычислить в compile-time или нет. Собственно, CTFE в D наглядно показывает, что это возможно.


                                                                                  Тем не менее, я могу себе представить что у комитета и у компиляторостроителей были вполне себе веские причины обязать использовать constexpr.


                                                                                  Так что сделали и сделали. Лично мне принесли существенную пользу, за что комитету большое спасибо.


                                                                                  А вот ваша ярость не понятна от слова совсем.

                                                                                    +1
                                                                                    Тем не менее, я могу себе представить что у комитета и у компиляторостроителей были вполне себе веские причины обязать использовать constexpr.
                                                                                    Так «вы можете себе представить» или «вы можете о них рассказать»?

                                                                                      0
                                                                                      Могу представить, что есть.
                                                                                        0

                                                                                        По поводу причин, фрагмент выступления Антона Полухина на CoreHard Autumn 2018: https://youtu.be/MXEgTYDnfJU?t=637


                                                                                        Собственно, там весь доклад посвящен особенностям constexpr в C++, если кто не видел, то можно найти минут 40 своего времени и ознакомится.

                                                                                    0
                                                                                    Вон, в OCaml, рекурсивные функции нужно let rec помечать и ничего: выглядит коряво, зато работает.

                                                                                    Но в окамле для этого есть типотеоретические обоснования (насколько я слышал). И в хаскеле, кстати, уже не нужно.

                                                                                    0

                                                                                    Директива constexpr означает, что функция может быть выполнена во время компиляции. Директива consteval — что функция должна быть выполнения во время компиляции. Если хотите, чтобы constexpr-функция была выполнена во время компиляции, то просто поместите результат её работы в constexpr-переменную.


                                                                                    Пример: вычисление квадратного корня. В случае, когда есть только consteval, придётся объявлять две функции: одну обычную, а одну — времени компиляции, и вызывать их явно.

                                                                                      0
                                                                                      Директива
                                                                                      constexpr
                                                                                      означает, что функция может быть выполнена во время компиляции.
                                                                                      Дык и любая другая функция тоже может быть выполнена во время компиляции. Даже если она не static и не inline. Если компилятор так захочет.

                                                                                      А если не захочет — так и constexpr-функция может отказаться выполняться в компайл-тайме.

                                                                                      Пример: вычисление квадратного корня. В случае, когда есть только consteval, придётся объявлять две функции: одну обычную, а одну — времени компиляции, и вызывать их явно.
                                                                                      Это великолепный пример. Потому что:
                                                                                      1. Про него говорят последгние лет 10.
                                                                                      2. Компилятор прекрасно извлекает корни в компайл-тайме.
                                                                                      3. Однако при этом использовать корень для создания массива — я не могу
                                                                                      Ну и кому стало легче от наличия constexpr? Без которого, заметьте, всё работало бы автоматически.
                                                                                        –2

                                                                                        Вы ошибку компиляции-то видели? 
Всё прекрасно компилируется:


                                                                                        


char buf[(int)sqrt(9)];

                                                                                        Дык и любая другая функция тоже может быть выполнена во время компиляции. Даже если она не static и не inline. Если компилятор так захочет.

                                                                                        Директива constexpr имеет тот же смысл, что и override.


                                                                                        Если функция объявлена как constexpr, то не заглядывая внутрь функции, вы можете быть уверенными, что она удовлетворяет ряду требований и может быть вычислена во время компиляции при определённых условиях.


                                                                                        Конечно, можно обойтись и без этой директивы — компилятор сам прекрасно разберётся, можно ли вычислить функцию в процессе компиляции. Проблема же в том, что тогда проверка на constexpr будет осуществляется не при определении функции, а при её использовании.

                                                                                          +2
                                                                                          Вы ошибку компиляции-то видели?
                                                                                          Не обратил внимания. В GCC значит эти функции объявлены как constexpr (что, вообще говоря, нарушение стандарта, хотя и приятное). Clang так не умеет — хотя корень считает.

                                                                                          Если функция объявлена как constexpr, то не заглядывая внутрь функции, вы можете быть уверенными, что она удовлетворяет ряду требований и может быть вычислена во время компиляции при определённых условиях.
                                                                                          Вовсе не обязательно. Это просто пометка: «мамой клянусь, эту функцию, если приспичит, таки можно будет вызвать при некоторой комдинации входных аргументов». Компилятор этого проверить не может (строго говоря это вообще невозможно: проблема остановки, как всегда).

                                                                                          Директива constexpr имеет тот же смысл, что и override.
                                                                                          Нет, нет и нет. Вот если бы constexpr обозначал то, что означает consteval, а для вычисления функции во время компиляции не нужно было бы писать ничего — аналогия была бы полная. И это было бы хорошо. А так как это сделано сейчас — это голованя боль для разработчиков — и без каких-либо гарантий.

                                                                                          Проблема же в том, что тогда проверка на constexpr будет осуществляется не при определении функции, а при её использовании.
                                                                                          А сейчас где это осуществляется?

                                                                                          Вы на пример-то смотрели?
                                                                                          #include <iostream>
                                                                                          
                                                                                          constexpr int this_is_stupid(bool stupid_arg = true) {
                                                                                              if (stupid_arg) {
                                                                                                std::cout << "WTH is this?" << std::endl;
                                                                                              }
                                                                                            return 42;
                                                                                          }
                                                                                          
                                                                                          int main() {
                                                                                              this_is_stupid();
                                                                                          }
                                                                                          Можно завести массив
                                                                                          char buf[this_is_stupid(false)];
                                                                                          а такой вот нельзя:
                                                                                          char buf[this_is_stupid(true)];
                                                                                          Так где у нас проверка, а?

                                                                                          Извините, но constexpr для функций — это ошибка дизайна. Такая же, как взаимодействие if constexr и static_assert. Вы действительно считаете, что вот это вот:
                                                                                          template<class T> struct dependent_false : std::false_type {};
                                                                                          template <typename T>
                                                                                          void f() {
                                                                                               if constexpr (std::is_arithmetic_v<T>)
                                                                                                   // ...
                                                                                               else
                                                                                                 static_assert(dependent_false<T>::value,
                                                                                                               "Must be arithmetic");
                                                                                          }
                                                                                          Чем-то лучше, чем static_assert(false)? Ну бред же, честное слово!

                                                                                          Поймите, я не против C++, я понимаю, что при всех его недостатках это — часто наилучший выбор. Но многие вещи в нём это… не хочу материться, да…
                                                                                            0

                                                                                            Послушайте, ну объясните, наконец, что у вас лично за проблемы с constexpr. А то пока складывается ощущение, что это у вас что-то совсем личное и иррациональное. Кроме того, из ваших эмоциональных комментариев не понятно, что и как должно было быть сделано, чтобы раздражения у вас не вызывало.


                                                                                            Функция без constexpr — это указание компилятору, что пытаться считать еще в compile-time нельзя. Тогда как написав constexpr разработчик говорит "вот это я хочу иметь доступным и в compile-time".


                                                                                            То, что внутри constexpr может быть что-то, что воспрепятствует возможности вычислить функцию в compile-time… Ну, shit happens, что поделать (примеры подобного легко отыскиваются и в других местах в C++). В конце-концов вся идеология C++ такова, что компилятор верит в то, что программист знает, что делает.


                                                                                            Кстати, может быть в вашем примере проблема лишь в невнятной диагностике, которую выдают текущие версии gcc/clang. А вот vc++ 15.9.3 дает вполне себе понятное описание проблемы:


                                                                                            t2.cpp(10): error C2131: expression did not evaluate to a constant
                                                                                            t2.cpp(5): note: failure was caused by call of undefined function or one not declared 'constexpr'
                                                                                            t2.cpp(5): note: see usage of 'std::operator <<'
                                                                                              +3
                                                                                              В конце-концов вся идеология C++ такова, что компилятор верит в то, что программист знает, что делает.
                                                                                              Ура-ура-ура. То есть вы всё-таки знаете принципы, на которых основан C++ немного.

                                                                                              Функция без constexpr — это указание компилятору, что пытаться считать еще в compile-time нельзя.
                                                                                              Да, но нет. Функция без constexpr — это просто функция без constexpr. Вот если бы для того, чтобы гарантировать, что функция не будет вызываться в тех контекстах, где нужно вычислять во время компиляции нужно было бы писать что-нибудь типа [[runtime]] — тогда да, можно было бы сказать, что это указание компилятору. А пустая последовательность символов ну никак не может быть указанием.

                                                                                              Тогда как написав constexpr разработчик говорит «вот это я хочу иметь доступным и в compile-time».
                                                                                              Нет. Написав constexpr вы убеждаете компилятор, который вам не хочет верить в том, что он должен пойти и посмотреть — а вдруг в данном конкретном случае функцию всё-таки можно вызвать во время компиляции? То есть это такой сертификат полноценности: да, я не тварь дрожащая, я право на твоё внимание имею!

                                                                                              Ну и как это соотносится с принципом, который вы сами же постулировали?
                                                                                                –2
                                                                                                > Ура-ура-ура.

                                                                                                Я не был бы столь радостен, поскольку вы, походу, пытаетесь разговаривать сами с собой и пишете тексты, которые ничего не поясняют для ваших собеседников.

                                                                                                Пока из ваших комментариев мне так и не стало ясно ни что конкретно вам не нравится в constexpr, ни что вам бы понравилось.

                                                                                                А с учетом вашей манеры излагать мысли я вообще сомневаюсь, что вы это сможете сформулировать.
                                                                                                  0

                                                                                                  Нет, написав constexpr, вы заставляете компилятор в момент объявления функции проверить, удовлетворяет ли функциям определённым ограничениям. Это немного упрощает отладку кода.


                                                                                                  Вы можете считать, что constexpr — лишнее слово, но когда constexpr только появился, требования к функциям, которые могут быть вычислены в процессе компиляции, были очень жёсткие. Сейчас эти требования постепенно смягчаются, но constexpr уже никуда не денется. Это станет legacy, с которым придётся мириться.

                                                                                                    +2
                                                                                                    Вы можете считать, что constexpr — лишнее слово, но когда constexpr только появился, требования к функциям, которые могут быть вычислены в процессе компиляции, были очень жёсткие. Сейчас эти требования постепенно смягчаются, но constexpr уже никуда не денется.
                                                                                                    А с этим я как раз не спорю. Типичный случай ситуации «хотели как лучше, а получилось как всегда». Когда во время компиляции могли исполняться только функции очень ограниченного вида и любой static_assert, вставленный в такую функцию делал невозможным использование её во время компиляции — эта пометка имела смысл. Чтобы не было сюрпризов, когда Вася добавил в функцию простую проверку — а у Пети программа перестала собираться.

                                                                                                    Когда же в языке этих проблем нет (в C++20, вроде как, даже constexpr new будет) — то эта пометка, ничего, в сущности, всё равно не гарантирующая, выглядит, как минимум, странно.

                                                                                                    Это станет legacy, с которым придётся мириться.
                                                                                                    Вот это-то и огорчает.

                                                                                                    Поскольку совместимость ломать нельзя — то подобную грязь из языка крайне сложно вычистить. Может быть после появления модулей можно будет заняться, наконец, расчисткой этого всего. Сейчас это невозможно, так как любой заголовочный файл должен одинаково компилироваться в любом компоненте программы. Если заголовочных файлов не будет — то можно будет разрешить в новых модулях constexpr не писать (просто считать, что все функции, к которым можно добавить constexpr и получить после этого валидную программу — уже неявно эту пометку имеют).
                                                                                              0

                                                                                              del, khim написал более хороший пример.

                                                                                          +3
                                                                                          Ну и кому, нафиг, всё это нужно? Если чтобы понять — будет что-то вычислено во время компиляции или нет всё равно нужно детективом работать?

                                                                                          А разгадка одна — безблагодатность однопроходный компилятор.


                                                                                          #include <array>
                                                                                          
                                                                                          int f() { return 3; }
                                                                                          
                                                                                          int main()
                                                                                          {
                                                                                              std::array<int, f()> arr;
                                                                                          }

                                                                                          не работает.


                                                                                          #include <array>
                                                                                          
                                                                                          auto f = [] { return 3; };
                                                                                          
                                                                                          int main()
                                                                                          {
                                                                                              std::array<int, f()> arr;
                                                                                          }

                                                                                          работает (C++17 и новее).

                                                                                    0
                                                                                    Ага, а играть в хоккей надо начинать с художественной гимнастики. Что тренируешь, то и развивается, лучше разбираться с тем, что нужно, чем тратить тоже время на то, что не пригодиться.
                                                                                      0
                                                                                      Когда изучал язык С, а потом переходишь к С++, то реально ценишь возможности, которые предоставляет С++. Да и какие-то базовые понятия все-таки проще изучать в С: работа с файлами, многопоточность и т.д.
                                                                                      У меня первым языком программирования в институте был С, о чем ни чуть не жалею.

                                                                                      На мой взгляд, до начала изучения С++ будет неплохо знать хоть какой-то язык программирования.
                                                                                        0
                                                                                        Статья неплохая и интересная. А вот комментаторы странные. Люди, почему Вы спорите чей язык лучше? Ну серьёзно? Кто любит Delphi, пусть пишет на Delphi, кто любит C# пусть пишет на C#, кто любит С++,… ну вы принцип поняли. Если Вам жизненно важно заставить другого человека писать не на том, что ему нравится, а на том, что нравится Вам, то, поздравляю, у Вас психологические проблемы. Сходите к психотерапевту.

                                                                                        Ну серьёзно. Объясните мне кто-нибудь: «Почему так обязательно заставлять другого принимать твою точку зрения, если тебе от этого ничего не будет?» Ладно, если бы мне платили за агитацию некоторого языка, то я бы изо всех сил агитировал бы. Но за так… Зачем силы-то тратить?
                                                                                          0
                                                                                          А зачем на Хабр заходить и вообще чего-то писать?
                                                                                            0

                                                                                            Да я не в этом смысле. Обсуждать — это другое дело. Я не понимаю тех, кто из последних сил пытается всем доказать, что именно его язык лучший.

                                                                                              0
                                                                                              А. Это не знаю. У меня как раз основной язык — C++, но я очень хорошо понимаю, что многие вещи в нём сделаны неразумно. По крайней мере если смотреть из сегодняшнего дня…
                                                                                                0
                                                                                                У меня основного нет. Я работаю на: C, С++, C#, Delphi, VHDL, Verilog, Java. И это только те, на которых я реально работаю в данный момент. Работаю я на них всех не от того, что я так крут (скорее всего это не так), а от того, что все они чем-то хороши. В зависимости от задачи лучше подходит тот или иной. Можно, конечно, их по сравнивать. Но зачем из кожи вон лезть, что бы что-то доказать?
                                                                                                  0
                                                                                                  В некоторых случаях и выбора-то нет. Verilog на C# сложно заменить.
                                                                                          0
                                                                                          А мне говорили, что С++ в этом веке сделали более удобным…
                                                                                            0
                                                                                            Вот такой вот парадокс: почитаешь форумы в Интернетах и складывается ощущение, что C++ и раньше был говном, а уж сейчас-то его точно сделали только хуже, развитие языка давным-давно свернуло не туда, сам язык стал настолько сложным, что только интеллектуалы с IQ выше 170 смогут написать хотя бы HelloWorld, в комитете заседают оторванные от реальности фрики, поэтому комитет мало того, что добавляет в язык то, что никому не нужно, так и делает это максимально корявым и неестественным образом.

                                                                                            А на практике с каждым новым стандартом писать на С++ становится и проще, и удобнее.

                                                                                            Яркий пример — тот самый constexpr, на который выше вылили столько дерьма и даже обозвали «интеллектуальная мастурбация». Хотя в обычной работе ты просто его используешь и радуешься, что теперь такая фича в языке есть.
                                                                                              0
                                                                                              Хотя в обычной работе ты просто его используешь и радуешься, что теперь такая фича в языке есть.

                                                                                              Вы сравниваете два состояния: "до того как ввели constexpr" и "после того как ввели constexpr". А надо — три: "до того как ввели constexpr", "после того как ввели constexpr" и "что было бы, если бы constexpr ввели по-нормальному".

                                                                                                –1
                                                                                                Во-первых, вы забываете, что у истории нет сослагательного наклонения.

                                                                                                Во-вторых, вы, забываете, что C++ — это инструмент, который нужен здесь и сейчас (хотя, как обычно, хороший инструмент нужен был еще вчера). Мне, при разработке софта, важно то, что инструмент позволяет мне сделать. А фантазии о том, что могло бы быть, я оставлю для форумных болтунов.

                                                                                                В-третьих, пока в этом обсуждении никто еще не показал, как следовало бы сделать «по-нормальному». Яркие фразы вроде «интеллектуальная мастурбация» в расчет не принимаются.
                                                                                                  +2
                                                                                                  Вам об этом сказали несколько раз, прямым текстом. Могу повторить:

                                                                                                  Вариант 1. Разрешить в constexpr-выражениях вызовы любых inline-функций.
                                                                                                  Вариант 2. Запретить в constexpr-функциях делать то, что помешает использовать её в constexpr-контексте.
                                                                                                    –2
                                                                                                    Вариант 1. Разрешить в constexpr-выражениях вызовы любых inline-функций.

                                                                                                    Одна из претензий к constexpr-функциям в том, что там можно делать все, что угодно. И пример был приведен с обращением к std::cout-у. Если уж для constexpr-функций компилятор не может помешать делать такие вещи, то что уж говорить про обычные inline-функции.


                                                                                                    Так что тут либо крестик снимите, либо...


                                                                                                    Кроме того, хотелось бы услышать как возможность вызова произвольной inline-функции скажется на времени компиляции. Если эта фича увеличит время компиляции в 3 раза, а потребляемую компилятором память в 10 раз, то...


                                                                                                    Вариант 2. Запретить в constexpr-функциях делать то, что помешает использовать её в constexpr-контексте.

                                                                                                    Вы точно хотели сказать "использовать её в constexpr-контексте" или же хотели написать "использовать её не в constexpr-контексте"?

                                                                                                      +3
                                                                                                      Да, я написал именно то что хотел написать.
                                                                                                        –4
                                                                                                        Если constexpr-функции нельзя было использовать вне constexpr-контекста, то ценность самих constexpr-функций и удобство работы с ними сильно снизилось бы.

                                                                                                        Во-первых, если мне нужен расчет одной и той же величины и в compile-time, и в run-time, то нужно было бы делать две одинаковых функции. Только одна с пометкой constexpr, вторая — без.

                                                                                                        Во-вторых, как отлаживать constexpr, если вызывать их можно только в compile-time? Пока constexpr-функция может вызываться в run-time, я могу отлаживать ее обычными средствами и писать для нее unit-тесты.
                                                                                                          +2
                                                                                                          А кто предлагает запретить использовать constexpr-функции вне constexpr-контекста? Не читайте в моих комментариях того, чего в них нет.
                                                                                                            0
                                                                                                            Если на какой-то момент в constexpr-функции мне потребуется, скажем, отладочная печать, то мне на это время придется убирать отметку constexpr?
                                                                                                              +3
                                                                                                              Ух ты, первое возражение по делу! Вам следовало написать это три дня назад...

                                                                                                              Можно скопировать функцию, убрав у копии constexpr. Лично я бы предпочел делать вот так, чем иметь в языке непроверенные компилятором constexpr-функции. Всё-таки отладочная печать — это уже крайнее средство, я ей не каждый день пользуюсь.
                                                                                                                –3
                                                                                                                Вам следовало написать это три дня назад...

                                                                                                                Возражения по делу можно приводить только там, где это самое дело в наличии. Как только кто-то составил четкий список из пунктов без эмоциональной окраски, так сразу и появился предмет для разговора.


                                                                                                                Кстати, еще лучше было бы список претензий к constexpr из практики, а не из высказываний вида "по-моему, это сделано через… опу".


                                                                                                                Лично я бы предпочел делать вот так

                                                                                                                Ну так это ваше личное мнение, ничем не лучшее, чем мое или еще чье-то. Более того, вы вряд ли сможете чем-то подтвердить, что если бы реализовали то, что вам нравится, то это бы привело к лучшим результатам.


                                                                                                                Даже если вернуться к примеру с std::cout в constexpr-функции. По сути, вот такой же пример:


                                                                                                                constexpr size_t dimension(bool flag) { return flag ? 64u : 0u; }
                                                                                                                
                                                                                                                int main() {
                                                                                                                   char a[dimension(true)];
                                                                                                                }

                                                                                                                Здесь все формальности по поводу constexpr-сности соблюдены, но поменяв значение параметра для dimension вы получаете ошибку компиляции.


                                                                                                                И, с точки зрения практики, мне без разницы, почему вызванная мной constexpr-функция ломает компиляцию: потому, что в ней вызывается std::cout, или потому, что она возвращает недопустимое для моего контекста значение.

                                                                                                                +2
                                                                                                                Я бы предпочёл что-то типа встроенных в компилятор средств отладки. Оно и в consteval пригодится.

                                                                                            Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                                                                                            Самое читаемое