Недавно мне напомнили, почему я считаю плохой идеей давать новичкам 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++. Мы видим бородавки на теле любимых и всё равно любим их.

И это всё, что я могу сказать об этом.

Комментарии (100)


  1. MikeLP
    01.02.2019 18:17
    +1

    Мда… И потом они ругают Python, PHP, Node за странности.


    1. khim
      02.02.2019 03:35
      +2

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

      Но с какого перепугу в языках, которые жрут память как не в себя и работают со скоростью черепахи куча закидонов?


      1. MikeLP
        02.02.2019 05:41

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


        1. DarkWanderer
          02.02.2019 09:38
          +4

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


        1. DistortNeo
          02.02.2019 14:34
          +4

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


          1. andreylartsev
            02.02.2019 23:05

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

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

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

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


            1. PsyHaSTe
              03.02.2019 01:37
              +1

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

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


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


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


              1. khim
                03.02.2019 02:08
                +1

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

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

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


                1. PsyHaSTe
                  03.02.2019 02:15

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

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

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

                  Citation needed.

                  Вы так уверены, что через 1000 лет мы будем продолжать С++ пользоваться?


                  1. khim
                    03.02.2019 03:58
                    +3

                    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 обеспечивать совместимость. Не будете, будете играть в те игры, что и Вирт… ну получите то, что получите.


                    1. DistortNeo
                      03.02.2019 12:56

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

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


                      1. eao197
                        03.02.2019 12:59

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

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


                        1. DistortNeo
                          03.02.2019 13:49

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


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


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


                          1. eao197
                            03.02.2019 13:56

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

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

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

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


                            1. DistortNeo
                              03.02.2019 14:08

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

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


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

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


                              1. eao197
                                03.02.2019 14:11

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

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

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

                                Ну да, ну да.


                                1. DistortNeo
                                  03.02.2019 15:16

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

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


                                  1. eao197
                                    03.02.2019 16:43

                                    Т.е. вы не знаете, как ваша идея-фикс помогла бы проблеме натягивания генериков на уже существующий байт-код JVM.


                                    1. DistortNeo
                                      03.02.2019 17:17

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


                                      1. eao197
                                        03.02.2019 17:21
                                        -1

                                        На колу мочало, начинаем все сначала.

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


                                        1. mayorovp
                                          03.02.2019 17:44
                                          +1

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


                                        1. DistortNeo
                                          03.02.2019 17:54

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

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


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


                                          1. eao197
                                            03.02.2019 18:20
                                            -1

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

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


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

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


                                            1. DistortNeo
                                              03.02.2019 18:23
                                              +1

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


                                              1. eao197
                                                03.02.2019 18:47
                                                -1

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


                                                1. mayorovp
                                                  03.02.2019 19:12

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


                                                1. DistortNeo
                                                  03.02.2019 21:50

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


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


                                                  1. eao197
                                                    03.02.2019 21:58

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


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


                                                    1. DistortNeo
                                                      03.02.2019 22:04

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

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


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

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


                                                      1. eao197
                                                        03.02.2019 22:07
                                                        -2

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

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


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

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


                                                        1. DistortNeo
                                                          03.02.2019 22:11
                                                          +2

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

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


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

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


                                                        1. eao197
                                                          04.02.2019 13:11

                                                          > Так ведь, насколько я помню, как раз из-за 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. mayorovp
                                                            04.02.2019 13:25

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


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

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


                                                            1. eao197
                                                              04.02.2019 13:49
                                                              -1

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

                                                              По поводу вашего комментария остается только сказать, что Sun потеряла очень многое из-за того, что не привлекла вас к реализации генериков в Java. Уж вы-то… Как же ж иначе.


                    1. PsyHaSTe
                      03.02.2019 13:29

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

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

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

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


                      1. DistortNeo
                        03.02.2019 13:54

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


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


                      1. khim
                        03.02.2019 14:16

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

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

                        Тот же Go, скорее, привлёк любителей Java и Python, а не C++


                        1. PsyHaSTe
                          03.02.2019 14:25

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

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


                          1. eao197
                            03.02.2019 16:43

                            если альтернативами никто не пользуется

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


                    1. bentall
                      04.02.2019 04:37

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

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


                      1. khim
                        04.02.2019 14:13

                        Дык эта. Никто ж не спорит, что идеи Алгола умерли — часть их много куда перекочевала.


  1. bfDeveloper
    01.02.2019 18:58

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



  1. degs
    01.02.2019 19:16
    +2

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


    int x, y{};

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


    1. Sap_ru
      02.02.2019 04:54
      -9

      Если это не глобальная или статическая переменная, то «x» тоже будет 0 абсолютно на всех существующих компиляторах, совместимых с любым из стандартов. С нулевым количеством расходования тактов процессора.


      1. mayorovp
        02.02.2019 15:21
        +5

        Строго наоборот — нулём окажется глобальная или статическая переменная. А вот в локальной будет «лежать» мусор.


  1. aamonster
    02.02.2019 08:53
    +2

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


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


  1. technic93
    02.02.2019 10:48
    -1

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


    auto a = A{};

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


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


    1. Livid
      02.02.2019 12:19
      +6

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


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


      1. bogolt
        02.02.2019 22:53

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


      1. technic93
        03.02.2019 12:29

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


      1. technic93
        03.02.2019 12:41
        +1

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


    1. mapron
      02.02.2019 12:36
      +3

      Какой-то говнокод, как по мне, зачем там вообще auto?
      A a{};
      auto годится только для всяких мутных типов которые совсем лень писать вроде итераторов и прочих map::value_type с которыми легко ошибиться при ручном написании.


      1. playermet
        02.02.2019 14:05
        +2

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


        1. mapron
          02.02.2019 19:32

          Я раньше был большим поклонником использования auto, но сейчас прихожу к мысли что все хорошо в меру.
          Если в коде тип действительно вообще никак не влияет на понимание кода, если это какой-то там vector<string>, то auto может быть уместно. Если я только что сделал make_unique, то тоже тип и так очевиден. Во многих местах видеть тип реально сокращает время чтения кода. Многие пишут мол IDE, и так все видит, выводит типы, но я читаю код очень быстро, проматывая по странице в 2-3 секунды, и мне такие визуальные зацепки очень помогают.
          Если нужно убрать когнитивную сложность из кода, скрыв «мусорный» тип, то auto вполне в тему)


        1. PsyHaSTe
          03.02.2019 01:39
          +1

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


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


      1. DistortNeo
        02.02.2019 14:38

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


        auto a = A();

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


        A a();

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


        A a = A();

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


        1. khim
          02.02.2019 14:54
          -1

          А раскрыть глаза и посмотреть на то, что вам предлагают нельзя? Написано

          A a();
          действительно нельзя, а вот
          A a{};
          написать можно всегда.

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


          1. DistortNeo
            02.02.2019 15:17
            +2

            А раскрыть глаза и посмотреть на то, что вам предлагают

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


            1. eao197
              02.02.2019 15:53

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

              Предлагают вещи, которые облегчают жизнь. Вряд ли кто-то в 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). Но это очередная ошибка эволюции. Бывает.


              1. DistortNeo
                02.02.2019 16:22
                -2

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

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


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

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


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

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


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

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


                1. eao197
                  02.02.2019 17:00
                  +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 '}'

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

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


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

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


                  1. DistortNeo
                    02.02.2019 17:09

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

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


                    1. eao197
                      02.02.2019 17:10

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


                      1. DistortNeo
                        02.02.2019 18:02

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


                        T a = T();


                        1. eao197
                          02.02.2019 18:04

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


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


                          1. khim
                            02.02.2019 18:31

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

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


                            1. eao197
                              02.02.2019 18:33

                              > До C++17 компилятор имеет право выбирать.

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


                              1. khim
                                02.02.2019 20:40

                                До C++14 включительно считается, что конструируется два объекта (один с конструктором по умолчанию, потом его копия), но компилятор имеет право убирать конструкторы копирования, если единственным способом заметить его исчезновение — по его побочным эффектам.


                        1. khim
                          02.02.2019 18:22

                          А ничего, что эти записи не эквивалентны? И ваша конструкция не работает с объектами, которые нельзя копировать?


      1. technic93
        03.02.2019 12:34
        +1

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


        let a = A::new();


        1. DistortNeo
          03.02.2019 12:58

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


      1. Bronx
        04.02.2019 03:51

        У конструкции
        auto a = A{}
        есть большой плюс в сравнении с
        A a{};
        — в ней невозможно забыть проинициализировать переменную. Компилятор обязательно выругается, если вы напишете auto a;, но молча пропустит A a;.


  1. Kozel-007
    02.02.2019 12:14
    +7

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


    1. mapron
      02.02.2019 12:33
      +1

      Ну так а решение-то какое? Если учить в обратном порядке, получается Сишный код с кучей утечек, дурацких memset которые зануляют только первые 4 байта и т.д.

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


      1. Kozel-007
        02.02.2019 15:55
        +2

        Решение: сначала начинать с STL, потом уже переходить с базовым Сишным вещам. Пусть уж лучше пихают vector и shared_ptr куда не попадя, чем пишут программы, результат которых зависит от модели процессора (реальный случай в моей практике).
        Вчерашние студенты не пишут код, где критична производительность. Надо будет — разберутся. Я считаю, что жыр ради стабильности — нормальный размен.


    1. playermet
      02.02.2019 14:10
      +3

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


  1. eao197
    02.02.2019 12:57

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


  1. Dovgaluk
    02.02.2019 19:38
    +1

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


  1. dom3d
    02.02.2019 22:47

    Меня еще немножко бесит спецификатор const.
    Справа, слева, внутри…


    1. khim
      02.02.2019 23:22

      Гораздо хуже constexpr. В случае с переменным — понятно зачем он нужен, а вот в случае с функциями… лишняя сущность, на самом деле, только чтобы разработчиков мучить.


      1. mapron
        03.02.2019 10:12
        +1

        Тогда вам от осознания того, что теперь еще добавился consteval, вообще плохо станет.


        1. khim
          03.02.2019 14:29

          Вот как раз consteval накладывает на функцию ограничения и чего-то, всё-таки, значит. А constexpr - эта такая пометка "если надо - ну вычисли это в компакт-тайм, а если не очень или не получается... то и чёрт с ним - будет обычный вызов"...

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


      1. eao197
        03.02.2019 10:28
        +1

        а вот в случае с функциями… лишняя сущность

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


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

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


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


        1. khim
          03.02.2019 14:39
          +2

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

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

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

          Если бы constexpr значит то, что в C++ обозначает consteval — вопросов бы не было.


          1. eao197
            03.02.2019 16:50

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

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


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


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


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


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


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


            1. khim
              03.02.2019 17:36

              Вы путаете возможность что-то посчитать в компайл-тайм и объявление функции 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 должен был быть изначально. Но… сделали как сделали…


              1. DistortNeo
                03.02.2019 17:43

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

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


                1. khim
                  03.02.2019 19:34
                  +2

                  Чукча-не-читатель-чукча-писатель? С чего вся дискуссия началась, извините?


              1. eao197
                03.02.2019 18:16

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

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


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


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

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


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


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


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


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


                1. khim
                  03.02.2019 19:39
                  +1

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


                  1. eao197
                    03.02.2019 19:41

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


                  1. eao197
                    04.02.2019 09:30

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


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


          1. DistortNeo
            03.02.2019 17:40

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


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


            1. khim
              03.02.2019 19:51

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

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

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


              1. DistortNeo
                03.02.2019 22:08
                -2

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


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

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

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


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


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


                1. khim
                  04.02.2019 01:52

                  Вы ошибку компиляции-то видели?
                  Не обратил внимания. В 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++, я понимаю, что при всех его недостатках это — часто наилучший выбор. Но многие вещи в нём это… не хочу материться, да…


                  1. eao197
                    04.02.2019 08:56
                    -1

                    Послушайте, ну объясните, наконец, что у вас лично за проблемы с 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 <<'


  1. kypec56
    03.02.2019 12:00

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


  1. AnROm
    03.02.2019 19:51

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

    На мой взгляд, до начала изучения С++ будет неплохо знать хоть какой-то язык программирования.