std::for_each
Все мы знаем эту знаменитую шаблонную функцию из стандартной библиотеки. Она позволяет применить унарную операцию для каждого элемента последовательности.
Чем она хороша? В подходящих условиях она дает больше семантики по сравнению с "конкурентами".
Например, есть range-based-for цикл. Он записывается примерно так:
В этом подходе к обработке набора данных нет ничего плохого. Но нам позволено слишком много свободы. Мы можем выйти из цикла, перейти к следующей итерации в середине текущей и так далее. И уже сама эта возможность заставляет читающего код больше напрягаться и искать сложную логику.
Но если такой логики нет и мы просто делаем определенную операцию над каждым элементом, то создается совершенно лишнее напряжение, которого можно было бы избежать. И этому побегу поможет std::for_each.
Функция имеет явную семантику: для каждого элемента последовательности выполняется вот эта функция. И все. Думать много не нужно. Нужно просто понять, как преобразуется или обрабатывается элемент и дело в шляпе.
Но не каждый знает, что эта функция возвращает не void, а тот же тип унарной операции, что мы передали в нее. Значит мы можем использовать stateful операции, то есть функциональные объекты, и сохранять результат вычислений в этом объекте не используя никакие глобальные переменные, ссылки и прочее. Стандарт гарантирует, что возвращаемое значение for_each содержит финальное состояния функтора после завершения операций над всеми элементами.
Эта особенность может пригодиться, когда помимо обработки элемента необходимо собрать по ним статистику. Допустим, я хочу убрать из массива строк все пробелы и сосчитать, сколько в среднем на каждую строку приходится пробелов. И тут как бы вроде скорее всего наверное вероятно лучше std::transform подходит(по семантике основной операции), но все портит сбор статистики. Можно засунуть в трансформ лямбду со ссылкой на внешний счетчик, но по смыслу это уже не будет чистая трансформация строк. Поэтому можно подобрать менее точечный по предназначению алгоритм, но он лучше подходит этой ситуации. Единственное, что лямбду нельзя будет использовать.
Пример:
Здесь мы используем функтор SpaceHandler, для которого перегружен оператор круглые скобки. За счет чего мы в этом операторе может сохранять вычисления в поля класса SpaceHandler. Чем мы и воспользовались для подсчета статистики.
Большое неудобство с этими лямбдами, но нам пока не позволено доставать из них поля класса, так что выживаем, как можем.
Кстати, с С++20 std::for_each стал constexpr, что позволяет удобнее обрабатывать наборы данных во время компиляции .
Use proper tools. Stay cool.
#cppcore #cpp20 #algorithms
Все мы знаем эту знаменитую шаблонную функцию из стандартной библиотеки. Она позволяет применить унарную операцию для каждого элемента последовательности.
Чем она хороша? В подходящих условиях она дает больше семантики по сравнению с "конкурентами".
Например, есть range-based-for цикл. Он записывается примерно так:
for(auto & traitor: traitors) {
// exquisitely torture the traitor
}
В этом подходе к обработке набора данных нет ничего плохого. Но нам позволено слишком много свободы. Мы можем выйти из цикла, перейти к следующей итерации в середине текущей и так далее. И уже сама эта возможность заставляет читающего код больше напрягаться и искать сложную логику.
Но если такой логики нет и мы просто делаем определенную операцию над каждым элементом, то создается совершенно лишнее напряжение, которого можно было бы избежать. И этому побегу поможет std::for_each.
Функция имеет явную семантику: для каждого элемента последовательности выполняется вот эта функция. И все. Думать много не нужно. Нужно просто понять, как преобразуется или обрабатывается элемент и дело в шляпе.
Но не каждый знает, что эта функция возвращает не void, а тот же тип унарной операции, что мы передали в нее. Значит мы можем использовать stateful операции, то есть функциональные объекты, и сохранять результат вычислений в этом объекте не используя никакие глобальные переменные, ссылки и прочее. Стандарт гарантирует, что возвращаемое значение for_each содержит финальное состояния функтора после завершения операций над всеми элементами.
Эта особенность может пригодиться, когда помимо обработки элемента необходимо собрать по ним статистику. Допустим, я хочу убрать из массива строк все пробелы и сосчитать, сколько в среднем на каждую строку приходится пробелов. И тут как бы вроде скорее всего наверное вероятно лучше std::transform подходит(по семантике основной операции), но все портит сбор статистики. Можно засунуть в трансформ лямбду со ссылкой на внешний счетчик, но по смыслу это уже не будет чистая трансформация строк. Поэтому можно подобрать менее точечный по предназначению алгоритм, но он лучше подходит этой ситуации. Единственное, что лямбду нельзя будет использовать.
Пример:
struct SpaceHandler {
void operator()(std::string& str) {
auto new_end_it = std::remove_if(str.begin(), str.end(), [](const auto & ch){ return ch == ' ';});
space_count += str.size() - std::distance(str.begin(), new_end_it);
str.erase(new_end_it, str.end());
}
int space_count {0};
};
int main() {
std::vector<std::string> container = {"Ole-ole-ole ole", "C++ is great!",
"Just a random string just to make third elem"};
int i = std::for_each(container.begin(), container.end(), SpaceHandler()).space_count;
std::for_each(container.begin(), container.end(), [](const auto& str) { std::cout << str << std::endl;});
std::cout << "Average number of spaces is " << static_cast<double>(i) / container.size() << std::endl;
}
//Output
Ole-ole-oleole
C++isgreat!
Justarandomstringjusttomakethirdelem
Average number of spaces is 3.66667
Здесь мы используем функтор SpaceHandler, для которого перегружен оператор круглые скобки. За счет чего мы в этом операторе может сохранять вычисления в поля класса SpaceHandler. Чем мы и воспользовались для подсчета статистики.
Большое неудобство с этими лямбдами, но нам пока не позволено доставать из них поля класса, так что выживаем, как можем.
Кстати, с С++20 std::for_each стал constexpr, что позволяет удобнее обрабатывать наборы данных во время компиляции .
Use proper tools. Stay cool.
#cppcore #cpp20 #algorithms
Как узнать, что constexpr функция вычисляется в compile time
C constexpr функциями есть один прикол - они могут вычисляться и в рантайме, и компайлтайме. Но вот какой момент: мы же не зря пометили их constexpr. Нам важно, чтобы в тот момент, когда мы хотели разгрузить рантайм, он действительно разгружался. Не всегда просто бывает сходу понять, что все аргументы функции тоже являются вычислимыми во время компиляции. А это, собственно, одно из основных условий того, что и сама функция вычислится в это время.
Так вот интересно: а можно ли как-то убедиться в том, что выражение вычислено в compile-time?
На самом деле можно, для этого даже есть несколько вариантов. Не то, чтобы эти проверки нужны в продовом коде, но они могут быть полезны при отладке.
⚡️ Присвоить значение выражения constexpr переменной. Они могут инициализироваться только значениями, доступными на этапе компиляции, поэтому вам сразу же компилятор выдаст ошибку "constexpr variable must be initialized by a constant expression", если вы попытаетесь передать в функцию не constexpr значение.
Правда, здесь есть ограничение, что void выражения таким макаром не проверить.
⚡️ Поместить вызов функции в static_assert. Эта штука может проверять только вычислимые на этапе компиляции условия, поэтому компилятор опять же вам подскажет, облажались вы или нет.
Ограничение здесь даже еще жестче, чем в предыдущем пункте. Вы должны каким-то образом из возвращаемого значения функции получить булевое значение. Для тривиальных типов это довольно тривиальное преобразование, как бы тривиально это не звучало. Но для более сложных конструкций нужно будет чуть больше подумать.
⚡️ Использовать фичу С++20 - std::is_constant_evaluated(). Если коротко, то она позволяет внутри функции определить в каком контексте она вычисляется: constant evaluation context или runtime context. Здесь есть и практическая польза: в зависимости от контекста мы можем использовать constexpr-френдли операции(их набор довольно сильно ограничен и придется попотеть, чтобы что-то сложное реализовать) или обычные. Но для наших целей мы можем вот как использовать: мы можем заветвиться по контексту и вернуть из функции какое-то уникальное значение, которое соответствует только compile-time ветке. И уже по итоговому результату понять, когда произошли вычисления. А дальше уже набрасывать реальный код в ветки. Например:
Обратите внимание, что здесь используется обычный if, потому что в if constexpr is_constant_evaluated будет всегда возвращать true(в нем условие всегда в compile-time поверяется).
Наверняка, есть еще способы. Если знаете, напишите их в комментарии)
Check context of your life. Stay cool.
#cpp20 #cpp11 #compiler
C constexpr функциями есть один прикол - они могут вычисляться и в рантайме, и компайлтайме. Но вот какой момент: мы же не зря пометили их constexpr. Нам важно, чтобы в тот момент, когда мы хотели разгрузить рантайм, он действительно разгружался. Не всегда просто бывает сходу понять, что все аргументы функции тоже являются вычислимыми во время компиляции. А это, собственно, одно из основных условий того, что и сама функция вычислится в это время.
Так вот интересно: а можно ли как-то убедиться в том, что выражение вычислено в compile-time?
На самом деле можно, для этого даже есть несколько вариантов. Не то, чтобы эти проверки нужны в продовом коде, но они могут быть полезны при отладке.
⚡️ Присвоить значение выражения constexpr переменной. Они могут инициализироваться только значениями, доступными на этапе компиляции, поэтому вам сразу же компилятор выдаст ошибку "constexpr variable must be initialized by a constant expression", если вы попытаетесь передать в функцию не constexpr значение.
constexpr int JustRandomUselessFunction(int num) {
return num + 1;
}
int main() {
int usual_runtime_var = 0;
constexpr int error = JustRandomUselessFunction(usual_runtime_var);
//👆🏿Error: constexpr variable must be initialized by a constant expression
constexpr int constexpr_var = 5;
constexpr int ok = JustRandomUselessFunction(constexpr_var);
//👆🏿 OK since constexpr_var is constexpr
}
Правда, здесь есть ограничение, что void выражения таким макаром не проверить.
⚡️ Поместить вызов функции в static_assert. Эта штука может проверять только вычислимые на этапе компиляции условия, поэтому компилятор опять же вам подскажет, облажались вы или нет.
constexpr int JustRandomUselessFunction(int num) {
return num + 1;
}
int main() {
int usual_runtime_var = 0;
constexpr int constexpr_var = 5;
static_assert(JustRandomUselessFunction(usual_runtime_var));
//👆🏿 Error: static assertion expression is not an integral constant expression
static_assert(JustRandomUselessFunction(constexpr_var)); // OK
}
Ограничение здесь даже еще жестче, чем в предыдущем пункте. Вы должны каким-то образом из возвращаемого значения функции получить булевое значение. Для тривиальных типов это довольно тривиальное преобразование, как бы тривиально это не звучало. Но для более сложных конструкций нужно будет чуть больше подумать.
⚡️ Использовать фичу С++20 - std::is_constant_evaluated(). Если коротко, то она позволяет внутри функции определить в каком контексте она вычисляется: constant evaluation context или runtime context. Здесь есть и практическая польза: в зависимости от контекста мы можем использовать constexpr-френдли операции(их набор довольно сильно ограничен и придется попотеть, чтобы что-то сложное реализовать) или обычные. Но для наших целей мы можем вот как использовать: мы можем заветвиться по контексту и вернуть из функции какое-то уникальное значение, которое соответствует только compile-time ветке. И уже по итоговому результату понять, когда произошли вычисления. А дальше уже набрасывать реальный код в ветки. Например:
constexpr int CalculatePeaceNumber(double base)
{
if (std::is_constant_evaluated())
{
return 666; // That's how we can be sure about compile time evaluation
}
else
{
// some peaceful code
return 0; // true balance and peace
}
}
constexpr double pi = 3.14;
int main() {
const int result = CalculatePeaceNumber(pi);
std::cout << result << " " << CalculatePeaceNumber(pi) << std::endl;
}
//Output
666 0
Обратите внимание, что здесь используется обычный if, потому что в if constexpr is_constant_evaluated будет всегда возвращать true(в нем условие всегда в compile-time поверяется).
Наверняка, есть еще способы. Если знаете, напишите их в комментарии)
Check context of your life. Stay cool.
#cpp20 #cpp11 #compiler
Квиз
Сегодня будет совсем простенький квиз, предвосхищающий раскрытие очень занудной, но важной темы. Ответ скину, по-классике, вечером.
Какой будет вывод у этого кода?
Сегодня будет совсем простенький квиз, предвосхищающий раскрытие очень занудной, но важной темы. Ответ скину, по-классике, вечером.
#include <iostream>
int main() {
int i = 10;
std::cout << (i & 1 == 0);
}
Какой будет вывод у этого кода?
Нормальный человек подумает, что ответ на вопрос выше: 1. Но компьютер не человек, как и те, кто помнят наизусть приоритет операций😆. Эти машины ответят 0. Почему?
Потому что приоритет оператора сравнения больше приоритета битового И. Поэтому в начале сравнятся 1 и 0. Будет 0. А И с нулем будет всегда нуль. Ожидаемым результат будет, если расставить скобки:
Чтобы вас больше нельзя было перехитрить, приведу здесь приоритеты операций в С++.
1. Постфиксный инкремент/декремент:
2. Вызов функции:
3. Индексный доступ:
4. Доступ к члену:
5. Префиксный инкремент/декремент:
6. Операторы умножения:
7. Операторы сложения:
8. Побитовые сдвиги:
9. Операторы сравнения:
10. Операторы равенства:
11. Побитовое И:
12. Побитовое исключающее ИЛИ:
13. Побитовое ИЛИ:
14. Логическое И:
15. Логическое ИЛИ:
16. Тернарный оператор:
17. Операторы присваивания:
18. Оператор запятой:
Вообще, ставить скобки - самое универсальное правило, которое здесь можно придумать, чтобы не попадаться на такие приколы. Сильно не уверен, что стоит прям заучивать эти приоритеты. Скобки даже на уроках математики приучают ставить, поэтому это всем понятная нам концепция. А стихотворение забывается через 0.000234 секунды после прочтения (а иногда и во время). Выводы делайте сами.
Компилятор, кстати, может стать тут вашим помощником. Какие-то из них и без опций показывают ворнинг об опасности неожиданных результатов из-за порядка приоритета операций. Но чтобы было наверяка, просто добавьте опции компиляции -Wall -Wextra(ну или на крайняк -Wparentheses) и вам обязательно все покажут и расскажут,что вы за черт по жизни где вы могли бы облажаться.
Stay alert. Stay cool.
Потому что приоритет оператора сравнения больше приоритета битового И. Поэтому в начале сравнятся 1 и 0. Будет 0. А И с нулем будет всегда нуль. Ожидаемым результат будет, если расставить скобки:
#include <iostream>
int main() {
int i = 10;
std::cout << ((i & 1) == 0);
}
Чтобы вас больше нельзя было перехитрить, приведу здесь приоритеты операций в С++.
1. Постфиксный инкремент/декремент:
++
, --
2. Вызов функции:
()
3. Индексный доступ:
[]
4. Доступ к члену:
.
, ->
5. Префиксный инкремент/декремент:
++
, --
, унарный плюс (+
), унарный минус (-
), логическое отрицание (!
), побитовое дополнение (~
), разыменование (*
), взятие адреса (&
), приведение типа (static_cast
, dynamic_cast
, reinterpret_cast
, const_cast
), sizeof
6. Операторы умножения:
*
, /
, %
7. Операторы сложения:
+
, -
8. Побитовые сдвиги:
<<
, >>
9. Операторы сравнения:
<
, >
, <=
, >=
10. Операторы равенства:
==
, !=
11. Побитовое И:
&
12. Побитовое исключающее ИЛИ:
^
13. Побитовое ИЛИ:
|
14. Логическое И:
&&
15. Логическое ИЛИ:
||
16. Тернарный оператор:
? :
17. Операторы присваивания:
=
, +=
, -=
, *=
, /=
, %=
, <<=
, >>=
, &=
, ^=
, |=
18. Оператор запятой:
,
Вообще, ставить скобки - самое универсальное правило, которое здесь можно придумать, чтобы не попадаться на такие приколы. Сильно не уверен, что стоит прям заучивать эти приоритеты. Скобки даже на уроках математики приучают ставить, поэтому это всем понятная нам концепция. А стихотворение забывается через 0.000234 секунды после прочтения (а иногда и во время). Выводы делайте сами.
Компилятор, кстати, может стать тут вашим помощником. Какие-то из них и без опций показывают ворнинг об опасности неожиданных результатов из-за порядка приоритета операций. Но чтобы было наверяка, просто добавьте опции компиляции -Wall -Wextra(ну или на крайняк -Wparentheses) и вам обязательно все покажут и расскажут,
Stay alert. Stay cool.
Мюсли подписчиков
Запускаем еще одну рубрику на канале: мюсли подписчиков. Здесь будут публиковаться их полнотекстовые статьи или небольшая превьюха к статье с ссылкой на оригинал.
Сегодняшним дебютантом будет наш добрый ленивец - @topin89.
Настройка окружения для запуска приложений - вещь далеко нетривиальная. Практически во всех проектах нужно от нескольких часов до нескольких дней, чтобы правильно проставить все нужные зависимости, установить все нужные приблуды и утилиты, чтобы только начать разрабатывать на новой машине. Это конечно никуда не годится. Вот можно было бы все настроить один раз и копипастить на все другие машинки все настройки сразу не тратить драгоценное время...
И такой способ есть! Называется контейнеризация. Обычно этот термин ассоциируется с каким-нибудь Docker'ом. Однако он не совсем подходит для создания окружения-как-ОС(рабочего окружения).
Автор статьи предлагает вам попробовать более подходящий инструмент для этого - systemd-nspawn. Хотя это даже не статья, а гайд по установке, настройке и минимальному запуску контейнеров с рабочим окружением. Причем такой гайд, что я прям охренел от степени проработки и подробности. Появился вопрос - далее по тексту определенно появится ответ. Все четко, структурировано и по делу. На все действия даны комментарии, так что вы точно будете знать, для чего предназначена та или иная строчка или команда.
В общем, крутой гайд от крутого специалиста и олда нашего канала. С вопросами по содержимому статьи можете приходить прямо к нему.
Ссылочка на статью вот. https://gist.github.com/topin89/f5078164e3dc53bd48838277baeb0d3b
Спасибо, Михаил, за то, что делитесь знаниями)
Share your knowledge. Stay cool.
#мюслиподписчиков
Запускаем еще одну рубрику на канале: мюсли подписчиков. Здесь будут публиковаться их полнотекстовые статьи или небольшая превьюха к статье с ссылкой на оригинал.
Сегодняшним дебютантом будет наш добрый ленивец - @topin89.
Настройка окружения для запуска приложений - вещь далеко нетривиальная. Практически во всех проектах нужно от нескольких часов до нескольких дней, чтобы правильно проставить все нужные зависимости, установить все нужные приблуды и утилиты, чтобы только начать разрабатывать на новой машине. Это конечно никуда не годится. Вот можно было бы все настроить один раз и копипастить на все другие машинки все настройки сразу не тратить драгоценное время...
И такой способ есть! Называется контейнеризация. Обычно этот термин ассоциируется с каким-нибудь Docker'ом. Однако он не совсем подходит для создания окружения-как-ОС(рабочего окружения).
Автор статьи предлагает вам попробовать более подходящий инструмент для этого - systemd-nspawn. Хотя это даже не статья, а гайд по установке, настройке и минимальному запуску контейнеров с рабочим окружением. Причем такой гайд, что я прям охренел от степени проработки и подробности. Появился вопрос - далее по тексту определенно появится ответ. Все четко, структурировано и по делу. На все действия даны комментарии, так что вы точно будете знать, для чего предназначена та или иная строчка или команда.
В общем, крутой гайд от крутого специалиста и олда нашего канала. С вопросами по содержимому статьи можете приходить прямо к нему.
Ссылочка на статью вот. https://gist.github.com/topin89/f5078164e3dc53bd48838277baeb0d3b
Спасибо, Михаил, за то, что делитесь знаниями)
Share your knowledge. Stay cool.
#мюслиподписчиков
Gist
systemd-nspawnd как рабочее место C++-программиста
systemd-nspawnd как рабочее место C++-программиста - systemd-nspawnd как рабочее место C++-программиста.md
Встраивание шаблонов
Небольшое предисловие: один из админов и автор последующих 5 постов уходит в отпуск на первые майские и будет недоступен. Выходить они будут отложенными публикациями. Так как мы особо не влияем на тексты друг друга, то второй админ может быть спокойно застан врасплох возможными неточностями постов и вашими комментариями. Так что прошу иметь это ввиду. Спасибо
Вот здесь мы поговорили о том, что методы класса - это по факту те же самые обычные функции, только для них первым параметром передается this. И если подумать 1.34 секунды, то можно понять, что взаимодействие класса с внешним миром происходит только за счет методов. А поля класса - это просто кусок памяти, из которого в разных ситуациях компилятор может достать ту или иную информацию. Получается, что низкоуровневый "код класса" - это набор низкоуровневого кода его методов(то есть обычных функций) и не более.
Получается, что возможна ситуация, когда компилятор встроит вызовы одного, нескольких или всех методов класса.
Шаблонные классы - хоть и неполноценные классы, но их инстанциации - да. Поэтому их методы также могут инлайниться, никаких исключений.
Обычные функции тоже могут встраиваться.
А константные шаблонные переменные после инстанциации могут не иметь имени, компилятор просто сразу подставит во все места использования конкретное значение.
Итого, получается, что у нас все шаблонные сущности могут быть встроены компилятором. Конечно же для этого должны быть включены оптимизации(но и без них может получиться).
Получается, что если мы в какой-то единице трансляции указываем явное объявление инстанциации с помощью extern template, и рассчитываем на неявную инстанциацию в другой единице трансляции, то мы спокойно можем нарваться на undefined reference.
Происходит это примерно так:
Знакомый пример, только пара модификаций. В хэдэре только объявление и определение шаблона. В ship.cpp пытаемся неявно инстанцировать строковую специализацию. Чтобы компилятор полностью не убирал код внутри foo за ненадобностью(тогда и ничего инстанцировать не нужно будет), сделаем так, чтобы она влияла на внешний мир. Добавим в шаблон поле, в методе его будем инкрементировать, и в foo выведем поле после модификации. В мэйне будем полагаться на инстанциацию в другой единице трансляции за счет extern template.
Вот если это попытаться скомпилировать(с оптимизациями) и собрать, то на линковке произойдет undefined reference. Компилятор увидел, что метод TurnShip слишком простой и его спокойно можно встроить и не генерировать для него определение. Что и происходит. А линкер в свою очередь из-за этого и не смог найти определение метода.
А божественным избавлением от этой проказы будет использование явной инстанциации. Она заставляет компилятор сгенерировать определение символа. Вызовы по прежнему могут инлайниться, но определение будет и мы сможем к нему обращаться.
Так что помните простое правило: на любое явное объявление инстанциации обязательно нужно предоставить явное определение инстанциации(1 на всю программу на каждое конкретное определение).
Rely on explicitly stated things. Stay cool.
#cppcore #template #compiler
Небольшое предисловие: один из админов и автор последующих 5 постов уходит в отпуск на первые майские и будет недоступен. Выходить они будут отложенными публикациями. Так как мы особо не влияем на тексты друг друга, то второй админ может быть спокойно застан врасплох возможными неточностями постов и вашими комментариями. Так что прошу иметь это ввиду. Спасибо
Вот здесь мы поговорили о том, что методы класса - это по факту те же самые обычные функции, только для них первым параметром передается this. И если подумать 1.34 секунды, то можно понять, что взаимодействие класса с внешним миром происходит только за счет методов. А поля класса - это просто кусок памяти, из которого в разных ситуациях компилятор может достать ту или иную информацию. Получается, что низкоуровневый "код класса" - это набор низкоуровневого кода его методов(то есть обычных функций) и не более.
Получается, что возможна ситуация, когда компилятор встроит вызовы одного, нескольких или всех методов класса.
Шаблонные классы - хоть и неполноценные классы, но их инстанциации - да. Поэтому их методы также могут инлайниться, никаких исключений.
Обычные функции тоже могут встраиваться.
А константные шаблонные переменные после инстанциации могут не иметь имени, компилятор просто сразу подставит во все места использования конкретное значение.
Итого, получается, что у нас все шаблонные сущности могут быть встроены компилятором. Конечно же для этого должны быть включены оптимизации(но и без них может получиться).
Получается, что если мы в какой-то единице трансляции указываем явное объявление инстанциации с помощью extern template, и рассчитываем на неявную инстанциацию в другой единице трансляции, то мы спокойно можем нарваться на undefined reference.
Происходит это примерно так:
// ship.hpp
#pragma once
template<typename T>
struct Ship
{
int i = 0;
void TurnShip(T command);
};
template <class T>
void Ship<T>::TurnShip(T command) {i++;}
// ship.cpp
#include "ship.hpp"
#include <string>
#include <iostream>
void foo() {
Ship<std::string> ship{};
ship.TurnShip(std::string{"Turn upside down"});
std::cout << ship.i << std::endl;
}
// main.cpp
#include "ship.hpp"
#include <string>
extern template class Ship<std::string>;
int main() {
Ship<std::string> ship;
ship.TurnShip(std::string{"Turn upside down"});
}
Знакомый пример, только пара модификаций. В хэдэре только объявление и определение шаблона. В ship.cpp пытаемся неявно инстанцировать строковую специализацию. Чтобы компилятор полностью не убирал код внутри foo за ненадобностью(тогда и ничего инстанцировать не нужно будет), сделаем так, чтобы она влияла на внешний мир. Добавим в шаблон поле, в методе его будем инкрементировать, и в foo выведем поле после модификации. В мэйне будем полагаться на инстанциацию в другой единице трансляции за счет extern template.
Вот если это попытаться скомпилировать(с оптимизациями) и собрать, то на линковке произойдет undefined reference. Компилятор увидел, что метод TurnShip слишком простой и его спокойно можно встроить и не генерировать для него определение. Что и происходит. А линкер в свою очередь из-за этого и не смог найти определение метода.
А божественным избавлением от этой проказы будет использование явной инстанциации. Она заставляет компилятор сгенерировать определение символа. Вызовы по прежнему могут инлайниться, но определение будет и мы сможем к нему обращаться.
Так что помните простое правило: на любое явное объявление инстанциации обязательно нужно предоставить явное определение инстанциации(1 на всю программу на каждое конкретное определение).
Rely on explicitly stated things. Stay cool.
#cppcore #template #compiler
Не всегда инстанциация шаблона нужна для работы программы
Возьмем пример из прошлого поста, объединим в хэдэре объявление шаблона с его определением и выкинем ship.cpp. И попробуем скомпилировать только main.cpp.
И неожиданно, все компилируется и выводится единичка. Почему так? Мы ведь почти ничего не поменяли даже просто нагло и беспардонно выкинули так нам необходимую единицу трансляции с явным инстанированием. Как это работает?
Дело в том, что любой метод, определенный внутри описания класса, неявно помечается inline. А на инлайн сущности не работает эффект подавления неявной специализации. Стандарт вот что говорит об этом:
Кажется, тут можно такую цепочку мыслей провести: компилятору запрещается делать неявную инстанциацию строкового корабля. Но он ее может и не делать, а просто встроить вызов метода этой инстанциации внутрь функции main и дело в шляпе! И ничего не нарушили и все работает.
Естественно, на это полагаться нельзя, потому что не любой метод может быть встроен, а значит компилятору придется проводить неявную инстанциацию. А мы как раз и добивались, чтобы этого не было. И правило "на любое явное объявление инстанциации обязательно нужно предоставить явное определение инстанциации" по-прежнему работает.
Просто интересно было показать, как такое небольшое изменение может развернуть ситуацию на 180. И кстати, если все-таки держать отдельно описание класса и его определение, но пометить метод inline, то будет тот же эффект, который я описал выше.
Pay attention to small details. Stay cool.
#template #compiler #cppcore
Возьмем пример из прошлого поста, объединим в хэдэре объявление шаблона с его определением и выкинем ship.cpp. И попробуем скомпилировать только main.cpp.
// ship.hpp
#pragma once
template<typename T>
struct Ship
{
int i = 0;
void TurnShip(T command) {i++;}
};
// main.cpp
#include "ship.hpp"
#include <string>
#include <iostream>
extern template class Ship<std::string>;
int main() {
Ship<std::string> ship;
ship.TurnShip(std::string{"Turn upside down"});
std::cout << ship.i << std::endl;
}
И неожиданно, все компилируется и выводится единичка. Почему так? Мы ведь почти ничего не поменяли даже просто нагло и беспардонно выкинули так нам необходимую единицу трансляции с явным инстанированием. Как это работает?
Дело в том, что любой метод, определенный внутри описания класса, неявно помечается inline. А на инлайн сущности не работает эффект подавления неявной специализации. Стандарт вот что говорит об этом:
Except for inline functions and class template specializations,
explicit instantiation declarations have the effect of suppressing
the implicit instantiation of the entity to which they refer.
Кажется, тут можно такую цепочку мыслей провести: компилятору запрещается делать неявную инстанциацию строкового корабля. Но он ее может и не делать, а просто встроить вызов метода этой инстанциации внутрь функции main и дело в шляпе! И ничего не нарушили и все работает.
Естественно, на это полагаться нельзя, потому что не любой метод может быть встроен, а значит компилятору придется проводить неявную инстанциацию. А мы как раз и добивались, чтобы этого не было. И правило "на любое явное объявление инстанциации обязательно нужно предоставить явное определение инстанциации" по-прежнему работает.
Просто интересно было показать, как такое небольшое изменение может развернуть ситуацию на 180. И кстати, если все-таки держать отдельно описание класса и его определение, но пометить метод inline, то будет тот же эффект, который я описал выше.
Pay attention to small details. Stay cool.
#template #compiler #cppcore
Преимущества использования extern template
C++ известен долгой компиляцией программ. И одной из причин такого поведения является наличие шаблонов в языке. За счет того, что стандарт допускает больше одной конкретной инстанциации шаблона в программе, люди сильно расслабляются. Мало кто знает, как правильно организовывать и использовать шаблонный код. В большинстве случаев он просто находится в хэдэре и подключается во всевозможные места. Соответсвенно, в каждой TU, куда подключается хэдэр, будет своя копия инстанциации. Но это же не просто сама копия, на компиляцию всех копий тратится время.
А если используются какие-нибудь кодогенераторы, типа grpc-шного или soap-ного, то там реально может быть очень много единиц трансляции. И в каждой будет своя копия какого-нибудь вектора или опшинала.
И хотя для STL-ных сущностей extern template ничем не поможет(об этом в другом посте), для самописных шаблонов, расположенных в хэдэрах - подойдет. Адекватную организацию кода при использовании extern template, мы осветили тут https://t.me/grokaemcpp/226.
Но какие конкретно преимущества дает такой способ организации шаблонного кода?
1) Уменьшение размеров объектных файлов скомпилированных единиц трансляции. При сборке больших проектов у вас скорее всего сгенерируются сотни, если не тысячи объектных файлов или либок, которые за счет загромождения инстанциациями могут весить десятки и сотни мегабайт. Не каждый себе может позволить на машинке такой билд, который помимо полезных исполняемых файлов будет содержать огромное количество объектников с дублирующимся миллион раз кодом. extern template запрещает неявную инстанциацию шаблонов, а значит среди всех объектников будет только один, который и будет содержать нужную инстанциацию.
2) Уменьшение времени компиляции. Компилятору запретили генерировать код, а значит много лишних действий убирается и сокращается время компиляции.
3) Сокращение времени линковки. Вот это не прям очевидный пункт. Чем объемнее объектные файлы - тем больше линкеру работы. А учитывая, что для дедуплицирования инстанциаций нужно найти эти одинаковые дубли, сопоставить их, выбрать один и убрать все остальные, то задача уже не кажется такой простой.
4) Любой код, который подключит ваш заголовочник, сможет инстанцировать или найти любой explicit template instantiation, который захочет. Через extern template мы запретим компилятору самостоятельно генерировать эти инстанциации. Но если для какого-то шаблонного параметра не будет прописан extern template, то компилятор сможет сам неявно конкретизировать шаблон с этим параметром. Поэтому в этом плане, это очень гибкий инструмент.
Из-за последнего пункта такой способ организации кода подойдет для частоиспользуемых шаблонов, которые инстанцируются с большим многообразием параметров. Просто в цппшнике можно предоставить самые популярные варианты инстанциаций, которые будут давать большой импакт в увеличении времени компиляции и наиболее часто будут дублироваться. Остальное компилятор сможет сам неявно инстанцировать.
Если внешняя библиотека завязана на ваш шаблон, то это единственный способ адекватно предоставить ей доступ к коду.
Однако есть способ организации кода, при котором будут все те же плюсы, плюс еще плюсы, минус лишней работы, но потеряем немного гибкости. Но об этом завтра.
Find benefits of using different things in various situations. Stay cool.
#cppcore #template #compiler
C++ известен долгой компиляцией программ. И одной из причин такого поведения является наличие шаблонов в языке. За счет того, что стандарт допускает больше одной конкретной инстанциации шаблона в программе, люди сильно расслабляются. Мало кто знает, как правильно организовывать и использовать шаблонный код. В большинстве случаев он просто находится в хэдэре и подключается во всевозможные места. Соответсвенно, в каждой TU, куда подключается хэдэр, будет своя копия инстанциации. Но это же не просто сама копия, на компиляцию всех копий тратится время.
А если используются какие-нибудь кодогенераторы, типа grpc-шного или soap-ного, то там реально может быть очень много единиц трансляции. И в каждой будет своя копия какого-нибудь вектора или опшинала.
И хотя для STL-ных сущностей extern template ничем не поможет(об этом в другом посте), для самописных шаблонов, расположенных в хэдэрах - подойдет. Адекватную организацию кода при использовании extern template, мы осветили тут https://t.me/grokaemcpp/226.
Но какие конкретно преимущества дает такой способ организации шаблонного кода?
1) Уменьшение размеров объектных файлов скомпилированных единиц трансляции. При сборке больших проектов у вас скорее всего сгенерируются сотни, если не тысячи объектных файлов или либок, которые за счет загромождения инстанциациями могут весить десятки и сотни мегабайт. Не каждый себе может позволить на машинке такой билд, который помимо полезных исполняемых файлов будет содержать огромное количество объектников с дублирующимся миллион раз кодом. extern template запрещает неявную инстанциацию шаблонов, а значит среди всех объектников будет только один, который и будет содержать нужную инстанциацию.
2) Уменьшение времени компиляции. Компилятору запретили генерировать код, а значит много лишних действий убирается и сокращается время компиляции.
3) Сокращение времени линковки. Вот это не прям очевидный пункт. Чем объемнее объектные файлы - тем больше линкеру работы. А учитывая, что для дедуплицирования инстанциаций нужно найти эти одинаковые дубли, сопоставить их, выбрать один и убрать все остальные, то задача уже не кажется такой простой.
4) Любой код, который подключит ваш заголовочник, сможет инстанцировать или найти любой explicit template instantiation, который захочет. Через extern template мы запретим компилятору самостоятельно генерировать эти инстанциации. Но если для какого-то шаблонного параметра не будет прописан extern template, то компилятор сможет сам неявно конкретизировать шаблон с этим параметром. Поэтому в этом плане, это очень гибкий инструмент.
Из-за последнего пункта такой способ организации кода подойдет для частоиспользуемых шаблонов, которые инстанцируются с большим многообразием параметров. Просто в цппшнике можно предоставить самые популярные варианты инстанциаций, которые будут давать большой импакт в увеличении времени компиляции и наиболее часто будут дублироваться. Остальное компилятор сможет сам неявно инстанцировать.
Если внешняя библиотека завязана на ваш шаблон, то это единственный способ адекватно предоставить ей доступ к коду.
Однако есть способ организации кода, при котором будут все те же плюсы, плюс еще плюсы, минус лишней работы, но потеряем немного гибкости. Но об этом завтра.
Find benefits of using different things in various situations. Stay cool.
#cppcore #template #compiler
Другой способ организации шаблонного кода
В чем недостаток способа из предыдущего поста? В том, что при любом изменении шаблона, придется перекомпилировать все единицы трансляции, которые его включают. Весь код находится в хэдэре, значит TU будет в себе содержать полное определение сущности. Значит, любое незначительное изменение реализации приводит к перекомпиляции.
Не зря люди придумали разделение кода на объявление сущности в хэдэре и ее реализацию в цппшнике. Внешний интерфейс класса/сигнатура функции меняются не так часто. А вот изменение деталей реализации при разработке - дело само собой разумеющееся и это происходит на каждой итерации билда при отладке кода.
При использовании подхода с разделением на хэдэр и сорец, при изменении реализации мы перекомпилируем только сорец. И все остальные TU не будут нуждаться в перекомпилировании. А при линковке они просто будут обращаться за нужными символами в обновленную TU исходников сущности.
Таким образом, мы хоть и не увеличиваем изначальную скорость компиляции, но увеличиваем ее при перекомпиляции исходников во время отладки. Я считаю, что это даже больший плюс. Потому что при отладке кода, мы его мильён раз изменяем и на каждую попытку ждать несколько минут - это зашквар. Так программисты и спиваются. Надо же что-то делать пока билд собирается?
Плюс из-за помещения определения в хэдэр, вы не можете распространять свои исходники без раскрытия деталей реализации. Код - бедная, тонкая, голая и стесняющаяся чужих людских глаз натура. Зачем вы его показываете на общее обозрение? Уважьте малыша. Спрячьте его.
Еще один плюс - удобство чтения кода и его организации. Очень часто нам нужно узнать только публичный интерфейс класса, без погружения в детали. Заголовочник с одним объявлением сущности сильно помогает в этом.
Ну и в мире коммерческой разработки на С++ в принципе принято разделение на сорцы и хэдэры. Поэтому, зачастую, инфраструктура проекта завязвна на этой негласной договоренности. И часто бывает непонятно, куда запихать обособленный заголовочник.
Единственный способ, при котором мы можем использовать разделение объявления и определения по разным файлам с шаблонами - мы должны точно знать, с какими параметрами будет инстанцироваться наш шаблон и в цппшнике предоставить явное его инстанцирование с этими параметрами.
И это большое ограничение. Но часто ли вы пишете шаблонный код, у которого вы прям не знаете полный набор возможных шаблонных параметров на данный момент? Средний разработчик не так часто это делает. И вот во всех случаях, когда вы на данный момент точно знаете полное множество возможных шаблонных параметров, стоит использовать именно тот способ, который описан в предыдущем абзаце или в этом посте.
И не нужно нигде писать никакой extern! Компилятор из объявления сам ничего не может инстанцировать, поэтому главная задача extern template решается автоматически.
Вы мне скажете, что нужно будет постоянно следить за новопоявившимися параметрами и добавлять явные инстанциации шаблона с ними в цппшник. И я вам отвечу: это правда. Просто в этом случае следить очень просто: у вас перестает компилироваться код)
Однако это лучше, чем в подходе в extern. Если здесь не уследить за новым шаблонным параметров, то компилятор сам сможет неявно с ним инстанцировать шаблон и тогда будут проявляться все негативные эффекты, которых мы и хотели избежать, использовав extern template.
Последние 2 поста - кульминация всей серии и реальная практическая выжимка из всего того, что мы уже обсудили. Выбирайте то, что подходит вашей ситуации. Надеюсь, я вас убедил в пользе явной инстанциации шаблонов. Еще один пост и мы заканчиваем этувсем уже немного поднадоевшую долгую историю.
Choose the proper tool. Stay cool.
#template #compiler #cppcore
В чем недостаток способа из предыдущего поста? В том, что при любом изменении шаблона, придется перекомпилировать все единицы трансляции, которые его включают. Весь код находится в хэдэре, значит TU будет в себе содержать полное определение сущности. Значит, любое незначительное изменение реализации приводит к перекомпиляции.
Не зря люди придумали разделение кода на объявление сущности в хэдэре и ее реализацию в цппшнике. Внешний интерфейс класса/сигнатура функции меняются не так часто. А вот изменение деталей реализации при разработке - дело само собой разумеющееся и это происходит на каждой итерации билда при отладке кода.
При использовании подхода с разделением на хэдэр и сорец, при изменении реализации мы перекомпилируем только сорец. И все остальные TU не будут нуждаться в перекомпилировании. А при линковке они просто будут обращаться за нужными символами в обновленную TU исходников сущности.
Таким образом, мы хоть и не увеличиваем изначальную скорость компиляции, но увеличиваем ее при перекомпиляции исходников во время отладки. Я считаю, что это даже больший плюс. Потому что при отладке кода, мы его мильён раз изменяем и на каждую попытку ждать несколько минут - это зашквар. Так программисты и спиваются. Надо же что-то делать пока билд собирается?
Плюс из-за помещения определения в хэдэр, вы не можете распространять свои исходники без раскрытия деталей реализации. Код - бедная, тонкая, голая и стесняющаяся чужих людских глаз натура. Зачем вы его показываете на общее обозрение? Уважьте малыша. Спрячьте его.
Еще один плюс - удобство чтения кода и его организации. Очень часто нам нужно узнать только публичный интерфейс класса, без погружения в детали. Заголовочник с одним объявлением сущности сильно помогает в этом.
Ну и в мире коммерческой разработки на С++ в принципе принято разделение на сорцы и хэдэры. Поэтому, зачастую, инфраструктура проекта завязвна на этой негласной договоренности. И часто бывает непонятно, куда запихать обособленный заголовочник.
Единственный способ, при котором мы можем использовать разделение объявления и определения по разным файлам с шаблонами - мы должны точно знать, с какими параметрами будет инстанцироваться наш шаблон и в цппшнике предоставить явное его инстанцирование с этими параметрами.
И это большое ограничение. Но часто ли вы пишете шаблонный код, у которого вы прям не знаете полный набор возможных шаблонных параметров на данный момент? Средний разработчик не так часто это делает. И вот во всех случаях, когда вы на данный момент точно знаете полное множество возможных шаблонных параметров, стоит использовать именно тот способ, который описан в предыдущем абзаце или в этом посте.
И не нужно нигде писать никакой extern! Компилятор из объявления сам ничего не может инстанцировать, поэтому главная задача extern template решается автоматически.
Вы мне скажете, что нужно будет постоянно следить за новопоявившимися параметрами и добавлять явные инстанциации шаблона с ними в цппшник. И я вам отвечу: это правда. Просто в этом случае следить очень просто: у вас перестает компилироваться код)
Однако это лучше, чем в подходе в extern. Если здесь не уследить за новым шаблонным параметров, то компилятор сам сможет неявно с ним инстанцировать шаблон и тогда будут проявляться все негативные эффекты, которых мы и хотели избежать, использовав extern template.
Последние 2 поста - кульминация всей серии и реальная практическая выжимка из всего того, что мы уже обсудили. Выбирайте то, что подходит вашей ситуации. Надеюсь, я вас убедил в пользе явной инстанциации шаблонов. Еще один пост и мы заканчиваем эту
Choose the proper tool. Stay cool.
#template #compiler #cppcore
extern template с шаблонами STL
Обещал рассказать, почему вы не сможете подавить неявную инстанциацию для STL сущностей. Это будет хорошим завершением серии, потому что включает множество обсужденных концепций и особенностей.
Оговорюсь, что буду говорить за реализацию стандартной библиотеки от gcc.
Начнем с того, что фичи стандартных шаблонов распространяются и подключаются с помощью хэдэр-файлов. И если внимательно посмотреть на эти хэдэра, то мы можем увидеть, что авторы библиотеки помещают методы шаблонных классов внутрь определения классов(за все-превсе классы не могу сказать, но тенденция явно проглядывается).
Далее. Единственным способом запретить неявное инстанцирование при распространении кода с помощью заголовочников - с помощью extern template. Все конкретизации мы не можем запретить, но самые популярные - можем попробовать. В случае успеха это будет давать большой импакт к уменьшению времени компиляции и размеру объектников.
А в недавнем посте "Не всегда инстанциация шаблона нужна для работы программы" мы поговорили о том, что методы, определенные внутри описания класса, неявно помечаются inline. А для inline функций запрет на неявное инстанцирование не работает!
Поэтому для шаблонов STL(под этим акронимом я понимаю все шаблонные классы стандартной библиотеки) вы просто не сможете получить никакого профита от extern template. Стандарт это явно запрещает.
Получается, что все разговоры про запрет или ограничение неявного инстанцирования касаются лишь только кастомного кода. Ограничения на стандартные инструменты наложить не получится, как и поиметь соотвествующих плюшек. Sad, but true. Об этой особенности очень мало говорят даже в англоязычном пространстве. Поэтому вы теперь элита и эксперты в правильной организации шаблонного кода.
Use your tools in proper places. Stay cool.
#cpcore #template #compiler
Обещал рассказать, почему вы не сможете подавить неявную инстанциацию для STL сущностей. Это будет хорошим завершением серии, потому что включает множество обсужденных концепций и особенностей.
Оговорюсь, что буду говорить за реализацию стандартной библиотеки от gcc.
Начнем с того, что фичи стандартных шаблонов распространяются и подключаются с помощью хэдэр-файлов. И если внимательно посмотреть на эти хэдэра, то мы можем увидеть, что авторы библиотеки помещают методы шаблонных классов внутрь определения классов(за все-превсе классы не могу сказать, но тенденция явно проглядывается).
Далее. Единственным способом запретить неявное инстанцирование при распространении кода с помощью заголовочников - с помощью extern template. Все конкретизации мы не можем запретить, но самые популярные - можем попробовать. В случае успеха это будет давать большой импакт к уменьшению времени компиляции и размеру объектников.
А в недавнем посте "Не всегда инстанциация шаблона нужна для работы программы" мы поговорили о том, что методы, определенные внутри описания класса, неявно помечаются inline. А для inline функций запрет на неявное инстанцирование не работает!
Поэтому для шаблонов STL(под этим акронимом я понимаю все шаблонные классы стандартной библиотеки) вы просто не сможете получить никакого профита от extern template. Стандарт это явно запрещает.
Получается, что все разговоры про запрет или ограничение неявного инстанцирования касаются лишь только кастомного кода. Ограничения на стандартные инструменты наложить не получится, как и поиметь соотвествующих плюшек. Sad, but true. Об этой особенности очень мало говорят даже в англоязычном пространстве. Поэтому вы теперь элита и эксперты в правильной организации шаблонного кода.
Use your tools in proper places. Stay cool.
#cpcore #template #compiler
Квиз
Возвращаемся в рабочие будни с интересным(надеюсь) #quiz'ом. Многие знают, что такой код писать нельзя, но не знают, почему и что будет в результате таких неправильных действий. Сегодня вечером и разберемся во всех тонкостях. А сейчас пытаемся сами понять, что будет при попытке запуска этого кода?
Возвращаемся в рабочие будни с интересным(надеюсь) #quiz'ом. Многие знают, что такой код писать нельзя, но не знают, почему и что будет в результате таких неправильных действий. Сегодня вечером и разберемся во всех тонкостях. А сейчас пытаемся сами понять, что будет при попытке запуска этого кода?
#include <iostream>
struct Class
{
Class(int init) : c{init}, b{c}, a{b} {}
int b;
int a;
int c;
};
int main() {
Class obj{5};
std::cout << obj.a << " " << obj.b << " " << obj.c << std::endl;
}
Результат попытки компиляции и запуска кода?
Anonymous Poll
14%
Ошибка компиляции
8%
5 {мусор} {мусор}
2%
{мусор} 5 {мусор}
45%
{мусор} {мусор} 5
12%
5 5 5
5%
{мусор} {мусор} {мусор}
0%
5 {мусор} 5
0%
5 5 {мусор}
12%
Пойду мусор выкину чтоль, а то чет на глаза часто попадется
Правильный ответ - {мусор} {мусор} 5.
Список инициализации в конструкторе - вещь дельная, полезная и иногда без нее никуда. Но с ним есть один неприятный момент - все может пойти по шляпе, если не соблюдать одно правило. "Порядок инициализации полей в списке инициализации конструктора должен совпадать с порядком следования полей в описании класса". Но вот не все знают, что же будет, если это правило нарушить. Поэтому давайте разбираться.
Для начала приведу цитату из стандарта:
Теперь разжуем эту сухую писанину в мягкую кашицу для лучшего усвоения.
В начале инициализируются базы класса в порядке, определяемом древовидной структурой наследования и ее проходом в глубину слева-направо. Не думаю, что сейчас надо что-то больше пояснять, как и про инициализацию статических полей. Ориентировочно 3.245 лайка на этот пост хватит, чтобы мы начали готовить про эти темы посты.
Далее инициализируются нестатические поля класса в порядке, в котором они объявлены в определении класса, и который никак не зависит от списка инициализации конструктора.
Ну и далее выполняется само тело конструктора.
Какие из этого выводы?
Самый главный - поведение абсолютно определено стандартом. Здесь нет никакого UB! Список инициализации лишь задает способ инициализации, но никак не влияет на порядок. Порядк определяется только порядком следования полей в описании класса(сверху вниз).
То есть в нашем примере в начале будет инициализировать
Никто нам не может запретить писать список инициализации в том порядке, в котором мы хотим. Да и непонятно, как это сделать. С точки зрения С++, код вполне корректен и может работать. Просто от этого будут последствия, но вполне предсказуемые.
Благо, к нам на помощь приходят компиляторы, которые аккуратно подскажут нам, что мы делаемчухню возможно не то, что хотим. Скорее всего компилятор вам сам кинет ворнинг, что переменная
Также вы можете(должны?) добавить флаг компиляции, чтобы превращать все предупреждения в ошибки. Тогда вы точно ничего важного не пропустите. Как говорится, флаг
Можете, кстати, поделиться в комментах историями, как у вас на проектах не стоял этот флаг, рекордным количеством неисправленных ворнингов и последствиями пренебрежением предупредпреждений.
Еще есть нюансы с default member initializer и его сочетанием со списком инициализации конструктора. Короче, много нюансов, как и во всех плюсах)
В один пост все все равно не влезет. Будем разделять и властвовать!
Devide et empera. Stay cool.
#cppcore
Список инициализации в конструкторе - вещь дельная, полезная и иногда без нее никуда. Но с ним есть один неприятный момент - все может пойти по шляпе, если не соблюдать одно правило. "Порядок инициализации полей в списке инициализации конструктора должен совпадать с порядком следования полей в описании класса". Но вот не все знают, что же будет, если это правило нарушить. Поэтому давайте разбираться.
Для начала приведу цитату из стандарта:
In a non-delegating constructor, initialization proceeds in the following order:
- First, and only for the constructor of the most derived class, virtual base classes
are initialized in the order they appear on a depth-first left-to-right traversal of the
directed acyclic graph of base classes, where “left-to-right” is the order of appearance
of the base classes in the derived class base-specifier-list.
- Then, direct base classes are initialized in declaration order as they appear in the
base-specifier-list(regardless of the order of the mem-initializer)
- Then, non-static data members are initialized in the order they were declared in the
class definition (again regardless of the order of the mem-initializer()
- Finally, the compound-statement of the constructor body is executed.
Теперь разжуем эту сухую писанину в мягкую кашицу для лучшего усвоения.
В начале инициализируются базы класса в порядке, определяемом древовидной структурой наследования и ее проходом в глубину слева-направо. Не думаю, что сейчас надо что-то больше пояснять, как и про инициализацию статических полей. Ориентировочно 3.245 лайка на этот пост хватит, чтобы мы начали готовить про эти темы посты.
Далее инициализируются нестатические поля класса в порядке, в котором они объявлены в определении класса, и который никак не зависит от списка инициализации конструктора.
Ну и далее выполняется само тело конструктора.
Какие из этого выводы?
Самый главный - поведение абсолютно определено стандартом. Здесь нет никакого UB! Список инициализации лишь задает способ инициализации, но никак не влияет на порядок. Порядк определяется только порядком следования полей в описании класса(сверху вниз).
То есть в нашем примере в начале будет инициализировать
b
, потом a
и, наконец, c
. И не смотря на то, что в списке инициализации c
стоит первым, на самом деле его инициализация будет проходить последней. А так как все остальные поля зависят от значения c
и инициализируются раньше него, то в них будет содержаться мусор.Никто нам не может запретить писать список инициализации в том порядке, в котором мы хотим. Да и непонятно, как это сделать. С точки зрения С++, код вполне корректен и может работать. Просто от этого будут последствия, но вполне предсказуемые.
Благо, к нам на помощь приходят компиляторы, которые аккуратно подскажут нам, что мы делаем
c
используется неинициализированной. Чтобы точно заставить компилятор это сделать, добавьте опцию -Wall
. Подробнее про ворнинги в этом цикле статей.Также вы можете(должны?) добавить флаг компиляции, чтобы превращать все предупреждения в ошибки. Тогда вы точно ничего важного не пропустите. Как говорится, флаг
-Werror
вам в руки!Можете, кстати, поделиться в комментах историями, как у вас на проектах не стоял этот флаг, рекордным количеством неисправленных ворнингов и последствиями пренебрежением предупредпреждений.
Еще есть нюансы с default member initializer и его сочетанием со списком инициализации конструктора. Короче, много нюансов, как и во всех плюсах)
В один пост все все равно не влезет. Будем разделять и властвовать!
Devide et empera. Stay cool.
#cppcore
Линковка constexpr с другими TU
Поступил в личку запрос от подписчика Сергея на пост по поводу линковки constexpr функций, которые используют статические переменные, с другими единицами трансляции. Чтож, будем рассказывать.
Для начала вспомним, что какое влияние ключевое слово constexpr оказывает на функции в плане линковки. Для функций constexpr подразумевает inline, поэтому мы уходит от первоначального вопроса к вопросу использования статических переменных в inline функциях.
Использование может быть разным. Сегодня рассмотрим использование статических констант в непосредственно в теле функции.
Приведу краткий рекап, но для более лучшего понимания процессов можете на достуге прочитать наш гайд по inline(ссылка в закрепе).
inline подразумевает внешнюю линковку. То есть другие единицы трансляции спокойно могут видеть определение сущности и взаимодействовать с ним. inline сущности могут иметь несколько определений в разных единицах трансляции. А компановщик после компиляции в итоговом бинарнике оставляет из всех лишь одно определение inline сущности.
Статические же сущности уникальны для каждой единицы компиляции и никому не позволительно иметь к ним доступ при линковке. Эдакие эндемики своей TU.
Еще инлайн функции имеют свойство иногда встраиваться в код caller'а. В случае, если в данной TU встроены все вызовы функции, то компилятор на оптимизациях может разрешить себе вообще не генерировать никакого определения.
И тут мы приходим в первой ситуации: мы определили inline функцию в одной единице трансляции и пытаемся из другой единицы получить к ней доступ. Условно так:
Это дело в таком виде не соберется даже без оптимизаций. Функция boo будет отсылаться на несуществующий символ gaga. Можно провести ряд манипуляций, чтобы в таком виде генерировалось определение, но на оптимизациях компилятор все равно его выкинет и сборка зафейлится.
Мы не знаем, какие алгоритмы компилятору говорят, можно ли встроить эту функций в ее вызов или нет. Поэтому я бы вообще такой код не писал и даже дальше в проблемы копать не нужно.
А они есть.
Более подходящим и общеиспользуемым вариантом организации кода с inline сущностями является помещение их в хэдэры и подключение в те TU, где они будут использоваться. Выглядит это примерно так:
Этот чудокод теперь собирается без проблем, компилятор встроит все вызовы и будет все хорошо. Но вот что будет, если функция gaga будет чуть сложнее для того, чтобы ее встраивать? Что будет, если для first.cpp и second.cpp компилятор все-таки будет генерировать определение gaga?
А будет UB. Тут применимо вот такое правило.
Но почему же определения разные? Мы же один и тот же код с одной и той же константой просто копируем в нужные единицы трансляции.
Только вот константы на самом деле разные. В каждой единице трансляции будет своя копия const_var и каждое определение gaga будет ссылаться на разные сущности-копии const_var.
В итоге останется одно определение функции, которое будет в себе содержать ссылку на локальную для единицы трансляции сущность. И любая другая единица трансляции может получается получить доступ к этой локальной сущности. Не уверен, что это вообще по-христиански.
Конечно, компилятор скорее всего оптимизирует использование такой простой переменной и все будет работать как ожидается. Но просто сам формат организации кода и зависимостей сущностей может привести к UB. Оно вам надо? Оно вам не надо.
Ярче эффекты могут проявиться не на константной переменной, а на обычной, изменяемой. Вот тут вы точно словите вагон и маленькое ведро неприятностей.
Поступил в личку запрос от подписчика Сергея на пост по поводу линковки constexpr функций, которые используют статические переменные, с другими единицами трансляции. Чтож, будем рассказывать.
Для начала вспомним, что какое влияние ключевое слово constexpr оказывает на функции в плане линковки. Для функций constexpr подразумевает inline, поэтому мы уходит от первоначального вопроса к вопросу использования статических переменных в inline функциях.
Использование может быть разным. Сегодня рассмотрим использование статических констант в непосредственно в теле функции.
Приведу краткий рекап, но для более лучшего понимания процессов можете на достуге прочитать наш гайд по inline(ссылка в закрепе).
inline подразумевает внешнюю линковку. То есть другие единицы трансляции спокойно могут видеть определение сущности и взаимодействовать с ним. inline сущности могут иметь несколько определений в разных единицах трансляции. А компановщик после компиляции в итоговом бинарнике оставляет из всех лишь одно определение inline сущности.
Статические же сущности уникальны для каждой единицы компиляции и никому не позволительно иметь к ним доступ при линковке. Эдакие эндемики своей TU.
Еще инлайн функции имеют свойство иногда встраиваться в код caller'а. В случае, если в данной TU встроены все вызовы функции, то компилятор на оптимизациях может разрешить себе вообще не генерировать никакого определения.
И тут мы приходим в первой ситуации: мы определили inline функцию в одной единице трансляции и пытаемся из другой единицы получить к ней доступ. Условно так:
//first.cpp
static const int a = 3;
constexpr int gaga() {
return a;
}
//second.cpp
int gaga();
void boo() {
gaga();
}
Это дело в таком виде не соберется даже без оптимизаций. Функция boo будет отсылаться на несуществующий символ gaga. Можно провести ряд манипуляций, чтобы в таком виде генерировалось определение, но на оптимизациях компилятор все равно его выкинет и сборка зафейлится.
Мы не знаем, какие алгоритмы компилятору говорят, можно ли встроить эту функций в ее вызов или нет. Поэтому я бы вообще такой код не писал и даже дальше в проблемы копать не нужно.
А они есть.
Более подходящим и общеиспользуемым вариантом организации кода с inline сущностями является помещение их в хэдэры и подключение в те TU, где они будут использоваться. Выглядит это примерно так:
//header.hpp
static const int const_var = 3;
constexpr int gaga() {
return const_var;
}
//first.cpp
#include "header.hpp"
void boo() {
gaga();
}
//second.cpp
#include "header.hpp"
void kak_delaut_gucy() {
gaga();
}
Этот чудокод теперь собирается без проблем, компилятор встроит все вызовы и будет все хорошо. Но вот что будет, если функция gaga будет чуть сложнее для того, чтобы ее встраивать? Что будет, если для first.cpp и second.cpp компилятор все-таки будет генерировать определение gaga?
А будет UB. Тут применимо вот такое правило.
If an inline function [...] with external
linkage is defined differently in
different translation units,
the behavior is undefined.
Но почему же определения разные? Мы же один и тот же код с одной и той же константой просто копируем в нужные единицы трансляции.
Только вот константы на самом деле разные. В каждой единице трансляции будет своя копия const_var и каждое определение gaga будет ссылаться на разные сущности-копии const_var.
В итоге останется одно определение функции, которое будет в себе содержать ссылку на локальную для единицы трансляции сущность. И любая другая единица трансляции может получается получить доступ к этой локальной сущности. Не уверен, что это вообще по-христиански.
Конечно, компилятор скорее всего оптимизирует использование такой простой переменной и все будет работать как ожидается. Но просто сам формат организации кода и зависимостей сущностей может привести к UB. Оно вам надо? Оно вам не надо.
Ярче эффекты могут проявиться не на константной переменной, а на обычной, изменяемой. Вот тут вы точно словите вагон и маленькое ведро неприятностей.
Фиксим неприятности
Сегодня коротко разберем, как обезопасить себя от проблем кода из предыдущего поста?
Просто надо использовать inline переменные! Но для этого понадобится С++17 и выше. Их и более менее все используют, но надо оговорку сделать.
Инлайн переменные также имеют внешнюю линковку, их определений может быть несколько в пределах одной программы, и по итогу компановщик также выберет одну из копий и весь остальной код будет ссылаться на нее. И в этом случае определение функции будет действительно единственным и доступным всем другим TU и это будет вполне легально.
Поэтому код будет выглядеть вот так:
constexpr и const имеют одинаковый линковочный смысл для переменных, поэтому замена вполне корректна. Да и просто constexpr переменная лучше смотрится с constexpr функций.
Fix your flaws. Stay cool.
#cpp17
Сегодня коротко разберем, как обезопасить себя от проблем кода из предыдущего поста?
Просто надо использовать inline переменные! Но для этого понадобится С++17 и выше. Их и более менее все используют, но надо оговорку сделать.
Инлайн переменные также имеют внешнюю линковку, их определений может быть несколько в пределах одной программы, и по итогу компановщик также выберет одну из копий и весь остальной код будет ссылаться на нее. И в этом случае определение функции будет действительно единственным и доступным всем другим TU и это будет вполне легально.
Поэтому код будет выглядеть вот так:
//header.hpp
inline constexpr int const_var = 3;
constexpr int gaga() {
return const_var;
}
//first.cpp
#include "header.hpp"
void boo() {
gaga();
}
//second.cpp
#include "header.hpp"
void kak_delaut_gucy() {
gaga();
}
constexpr и const имеют одинаковый линковочный смысл для переменных, поэтому замена вполне корректна. Да и просто constexpr переменная лучше смотрится с constexpr функций.
Fix your flaws. Stay cool.
#cpp17
Правильно смешиваем static с inline
На мой взгляд предыдущее решение проблемы хоть и очень крутое, модное и молодежное, но иногда можно и лучше.
Например. Бывают случаи, когда в глобальную область выносят переменные, которые на самом деле не глобальные. Вообще, глобальные переменные - не самый хороший признак архитектуры кода. Они могут относиться к конкретным сущностям в коде, которые уже обособлены или могут быть выделены в будущем. Существование свободных функций хоть и допустимо, но тоже всегда должно подвергаться сомнению. Возможно эти функции про какую-то отдельную сущность и их стоит выделить в класс. Тогда можно попробовать некоторые другие вещи, помимо заинлайнивания переменной.
Если свободные функции перенести внутрь описания класса и сделать их явно или неявно inline, то с точки зрения этой функции ничего не изменится. У нее также осталась внешняя линковка и в любой единице транляции будет ее определение.
Но вот теперь можно в ее теле попробовать использовать статические поля класса. Здесь мы обсудили, что они имеют внешнее связывание. Они либо аллоцированы в одной единице трансляции в случае если они не inline, либо имеют определение в нескольких в случае inline. Если используется обычное статического поля внутри инлайн функции, то во всех ее определениях будет содержаться единственный экземпляра этого поля и все определения в разных единицах трансляции будут одинаковые. А при использовании inline статической переменной, то компановщик объединит все ее копии в одну и в итоге все будут ссылаться на одну сущность.
Выглядеть это может примерно так:
Дальше. Не обязательно глобальная переменная принадлежит какому-то классу. Они может принадлежать самой этой функции и больше нигде не использоваться. А нужна она была для сохранения состояния между вызовами функции. Тут очень напрашивается просто поместить эту статическую глобальную переменную и тогда она станет статической локальной переменной. С константами это конечно абсолютно бессмысленно делать, но для "переменных" переменных можно и это стоит упоминания. Тогда мы уже не можем говорить про constexpr(будет зависимость от рантаймового значения), поэтому дальше разговор только про inline.
Статические локальные переменные не имеют никакой линковки(к ним нельзя получить доступ вне функции), поэтому не совсем понятно, корректно ли такая конструкция себя будет вести в инлайн функциях. И оказывается корректно(из cppreference):
Нам гарантируют, что все определения инлайн функции будут ссылаться на одну и ту же сущность-статическую локальную переменную.
Выглядеть это может так:
В общем, смешивать inline и static - можно, но очень осторожно. Не противоречьте стандарту и никакое UB не овладеет вашим кодом.
Mix things properly. Stay cool.
#cpp17 #cppcore #compiler
На мой взгляд предыдущее решение проблемы хоть и очень крутое, модное и молодежное, но иногда можно и лучше.
Например. Бывают случаи, когда в глобальную область выносят переменные, которые на самом деле не глобальные. Вообще, глобальные переменные - не самый хороший признак архитектуры кода. Они могут относиться к конкретным сущностям в коде, которые уже обособлены или могут быть выделены в будущем. Существование свободных функций хоть и допустимо, но тоже всегда должно подвергаться сомнению. Возможно эти функции про какую-то отдельную сущность и их стоит выделить в класс. Тогда можно попробовать некоторые другие вещи, помимо заинлайнивания переменной.
Если свободные функции перенести внутрь описания класса и сделать их явно или неявно inline, то с точки зрения этой функции ничего не изменится. У нее также осталась внешняя линковка и в любой единице транляции будет ее определение.
Но вот теперь можно в ее теле попробовать использовать статические поля класса. Здесь мы обсудили, что они имеют внешнее связывание. Они либо аллоцированы в одной единице трансляции в случае если они не inline, либо имеют определение в нескольких в случае inline. Если используется обычное статического поля внутри инлайн функции, то во всех ее определениях будет содержаться единственный экземпляра этого поля и все определения в разных единицах трансляции будут одинаковые. А при использовании inline статической переменной, то компановщик объединит все ее копии в одну и в итоге все будут ссылаться на одну сущность.
Выглядеть это может примерно так:
//header.hpp
struct StrangeSounds {
static constexpr int gaga() {
return krya;
}
static const int krya = 3;
};
//first.cpp
#include "header.hpp"
void boo() {
StrangeSounds::gaga();
}
//second.cpp
#include "header.hpp"
void kak_delaut_gucy() {
StrangeSounds::gaga();
}
Дальше. Не обязательно глобальная переменная принадлежит какому-то классу. Они может принадлежать самой этой функции и больше нигде не использоваться. А нужна она была для сохранения состояния между вызовами функции. Тут очень напрашивается просто поместить эту статическую глобальную переменную и тогда она станет статической локальной переменной. С константами это конечно абсолютно бессмысленно делать, но для "переменных" переменных можно и это стоит упоминания. Тогда мы уже не можем говорить про constexpr(будет зависимость от рантаймового значения), поэтому дальше разговор только про inline.
Статические локальные переменные не имеют никакой линковки(к ним нельзя получить доступ вне функции), поэтому не совсем понятно, корректно ли такая конструкция себя будет вести в инлайн функциях. И оказывается корректно(из cppreference):
Function-local static objects in all definitions
of the same inline function (which may be
implicitly inline) all refer to the same object
defined in one translation unit, as long as the
function has external linkage.
Нам гарантируют, что все определения инлайн функции будут ссылаться на одну и ту же сущность-статическую локальную переменную.
Выглядеть это может так:
//header.hpp
inline int gaga() {
static int krya = 3;
return krya++;
}
//first.cpp
#include "header.hpp"
int boo() {
return gaga();
}
//second.cpp
#include "header.hpp"
int kak_delaut_gucy() {
return gaga();
}
В общем, смешивать inline и static - можно, но очень осторожно. Не противоречьте стандарту и никакое UB не овладеет вашим кодом.
Mix things properly. Stay cool.
#cpp17 #cppcore #compiler
Инициализация статических полей класса. Ч1
Под этот пост вы накидали хренову гору лайков, поэтому разбираем дальше тему инициализации. Сегодня рассмотрим, как она проходит для классов со статическими полями.
Начнем с того, что все статические поля всех классов инициализируются до входа в main. Это и логично, ведь к таким полям мы в любой момент можем обратиться без объекта, просто по имени класса. Но вот в какой конкретно момент времени мы не можем сказать наверняка, это implementation defined штука. Каждый линкер вправе делать это по-своему. Единственное, что стандарт нам гарантирует:
Все статические объекты инициализируются в порядке, в котором они определены(не объявлены) в единице трансляции. Причем происхождение этого статического объекта не важно. Например, скомпилировав и запустив такой пример в main.cpp:
Мы в выводе получим следующее:
Несмотря на то, что в классе Class поле
Это происходит из-за того, что статические поля класса практически никак с этим классом не связаны. Доступ к ним разве что через имя класса. А так это обычная статическая переменная, как и любая другая.
Именно поэтому в цитате из стандарта говорится обобщенно о статических объектах. Для линкера вообще никакой разницы между переменными
С инициализацией статических полей и объектов еще много нюансов, будем потихоньку все разбирать.
Define order of your life. Stay cool.
#cppcore
Под этот пост вы накидали хренову гору лайков, поэтому разбираем дальше тему инициализации. Сегодня рассмотрим, как она проходит для классов со статическими полями.
Начнем с того, что все статические поля всех классов инициализируются до входа в main. Это и логично, ведь к таким полям мы в любой момент можем обратиться без объекта, просто по имени класса. Но вот в какой конкретно момент времени мы не можем сказать наверняка, это implementation defined штука. Каждый линкер вправе делать это по-своему. Единственное, что стандарт нам гарантирует:
Objects with static storage duration
defined in namespace scope in the same
translation unit and dynamically initialized
shall be initialized in the order in which
their definition appears in the translation unit.
Все статические объекты инициализируются в порядке, в котором они определены(не объявлены) в единице трансляции. Причем происхождение этого статического объекта не важно. Например, скомпилировав и запустив такой пример в main.cpp:
struct Helper {
Helper(int num) : data{num} {
std::cout << "Helper " << num << std::endl;
}
private:
int data;
};
struct Class {
static Helper a;
static Helper b;
};
Helper Class::b{1};
static Helper c{2};
Helper Class::a{3};
int main() {}
Мы в выводе получим следующее:
Helper 1
Helper 2
Helper 3
Несмотря на то, что в классе Class поле
a
было объявлено первым, инициализируется оно самым последним, потому что оно определено самым последним. А переменная c
вообще не относится к классу, но была проинициализирована между полями Class'а, потому что ее определение расположено между ними. Это происходит из-за того, что статические поля класса практически никак с этим классом не связаны. Доступ к ним разве что через имя класса. А так это обычная статическая переменная, как и любая другая.
Именно поэтому в цитате из стандарта говорится обобщенно о статических объектах. Для линкера вообще никакой разницы между переменными
a
, b
и c
нету. Это просто сущности с разными именами, но абсолютно равными правами. С инициализацией статических полей и объектов еще много нюансов, будем потихоньку все разбирать.
Define order of your life. Stay cool.
#cppcore
Инициализация статических полей класса. Ч2
Как думаете, может ли быть такое, что статическое поле класса инициализируется после завершения вызова конструктора класса? То есть объект уже создался, а статическое поле его класса еще не инициализированно? Подумайте пару секунд над этим вопросом самостоятельно.
После вчерашнего поста вам уже немного легче должны были даться рассуждения. Загвоздка в том, что статическое объекты инициализируются в порядке определения и не важно, какого класса эти объекты.
Посмотрим на пример.
Есть у нас класс, который сохраняет все свои инстансы по ключу в статическую мапу и все созданные инстансы доступны только через эту мапу.
Чтобы такое провернуть, создаем в классе статическую мапу, статический метод Create, который предоставляет доступ к созданию объектов и объявляем конструктор класса приватным, чтобы никто снаружи не смог втихаря создать объект. Таким образом, доступ к объектам есть только через метод Create и статическую мапу.
Не имея представления о нюансах инициализации, взбрело нам в голову создать и использовать объект перед определением мапы.
Запуская все это дело, получим сегфолт. И да, да, вы все правильно поняли. Все из-за порядка инициализации.
Так как линкеру пофиг на тип статических объектов, он спокойно может поставить инициализацию статического поля класса после завершения работы конструктора объекта в глобальном неймспейсе. Вот и получается конфуз: объект надо создавать, а используемое поле не инициализировано. От того и падаем.
Здесь ситуация игрушечная и довольно простая, потому что все находится в одной единице трансляции. Пример такой, потому что внутри одной единицы компиляции порядок инициализации детерминирован, тут легче показать причину и следствие. Но когда мы выходим за ее пределы и пытаемся создать объект InitializationTest в другой единице трансляции в глобальном скоупе, то поведение кода начинает зависеть от линкера. Порядок создания объектов между юнитами компиляции не определен и тут все будет, как решит компановщик. Можно конечно почитать документацию и действовать в соответствии с ней. Но этот код будет непереносим, а также не защитит вас от возможных изменений в поведении линкера.
Будьте аккуратны с инициализацией статических объектов и в принципе поменьше их используйте.
Be careful. Stay cool.
Как думаете, может ли быть такое, что статическое поле класса инициализируется после завершения вызова конструктора класса? То есть объект уже создался, а статическое поле его класса еще не инициализированно? Подумайте пару секунд над этим вопросом самостоятельно.
После вчерашнего поста вам уже немного легче должны были даться рассуждения. Загвоздка в том, что статическое объекты инициализируются в порядке определения и не важно, какого класса эти объекты.
Посмотрим на пример.
Есть у нас класс, который сохраняет все свои инстансы по ключу в статическую мапу и все созданные инстансы доступны только через эту мапу.
class InitializationTest {
public:
static std::map<std::string, std::unique_ptr<InitializationTest>> map;
static bool Create(std::string ID) {
map.insert({ID, std::move(std::unique_ptr<InitializationTest>{new InitializationTest})});
return true;
}
private:
Test() = default;
};
static bool creation_result = InitializationTest::Create("qwe");
// Somehow handle result and process object
std::map<std::string, std::unique_ptr<InitializationTest>> InitializationTest::map{};
int main() {}
Чтобы такое провернуть, создаем в классе статическую мапу, статический метод Create, который предоставляет доступ к созданию объектов и объявляем конструктор класса приватным, чтобы никто снаружи не смог втихаря создать объект. Таким образом, доступ к объектам есть только через метод Create и статическую мапу.
Не имея представления о нюансах инициализации, взбрело нам в голову создать и использовать объект перед определением мапы.
Запуская все это дело, получим сегфолт. И да, да, вы все правильно поняли. Все из-за порядка инициализации.
Так как линкеру пофиг на тип статических объектов, он спокойно может поставить инициализацию статического поля класса после завершения работы конструктора объекта в глобальном неймспейсе. Вот и получается конфуз: объект надо создавать, а используемое поле не инициализировано. От того и падаем.
Здесь ситуация игрушечная и довольно простая, потому что все находится в одной единице трансляции. Пример такой, потому что внутри одной единицы компиляции порядок инициализации детерминирован, тут легче показать причину и следствие. Но когда мы выходим за ее пределы и пытаемся создать объект InitializationTest в другой единице трансляции в глобальном скоупе, то поведение кода начинает зависеть от линкера. Порядок создания объектов между юнитами компиляции не определен и тут все будет, как решит компановщик. Можно конечно почитать документацию и действовать в соответствии с ней. Но этот код будет непереносим, а также не защитит вас от возможных изменений в поведении линкера.
Будьте аккуратны с инициализацией статических объектов и в принципе поменьше их используйте.
Be careful. Stay cool.
Инициализация статических полей класса. Ч3
В первой части мы разобрали порядок инициализации статических поле в случае их out-of-class определения. Однако в современных плюсах редко, кто вне описания класса инициализирует статические поля. Все потому что в С++17 появились инлайн переменные, которые позволяют не нарушать ODR при наличии их определения в разных единицах трансляции. Подробнее об этом тут. Эта фича позволила определять статические поля сразу внутри описания класса. Более подробно об этом тут.
Ну и встает вопрос: в каком порядке инициализируются inline static class members?
В целом, ответ такой же: в порядке определения. Только эти определения теперь совмещены с объявлением, поэтому можно сказать, что инициализация происходит в порядке появления этих полей в описании класса. Спасибо Артему Кузнецову, что указал в комментариях на эту особенность)
Ну и для того, чтобы пост был не таким скучным, давайте попробуем смешать обычные статические мемберы и инлайновые и посмотрим, как эта смесь будет себя вести.
Выглядит это примерно так:
Вывод будет таким:
В целом, картина довольно понятная. Если линкер ставит инициализацию статиков по порядку их определения, то здесь прослеживается та же история. Первыми инициализируются инлайны по порядку появления их в классе, а последним инициализируется неинлайновое поле, даже с учетом того, что оно объявлено между двумя первыми.
Так что порядок следования определений - наше все.
Rely on explicitly stated rules. Stay cool.
#cpp17 #cppcore
В первой части мы разобрали порядок инициализации статических поле в случае их out-of-class определения. Однако в современных плюсах редко, кто вне описания класса инициализирует статические поля. Все потому что в С++17 появились инлайн переменные, которые позволяют не нарушать ODR при наличии их определения в разных единицах трансляции. Подробнее об этом тут. Эта фича позволила определять статические поля сразу внутри описания класса. Более подробно об этом тут.
Ну и встает вопрос: в каком порядке инициализируются inline static class members?
В целом, ответ такой же: в порядке определения. Только эти определения теперь совмещены с объявлением, поэтому можно сказать, что инициализация происходит в порядке появления этих полей в описании класса. Спасибо Артему Кузнецову, что указал в комментариях на эту особенность)
Ну и для того, чтобы пост был не таким скучным, давайте попробуем смешать обычные статические мемберы и инлайновые и посмотрим, как эта смесь будет себя вести.
Выглядит это примерно так:
struct Helper {
Helper(int num) : data{num} {
std::cout << "Helper " << num << std::endl;
}
private:
int data;
};
struct Class {
static inline Helper a{1};
static Helper b;
static inline Helper c{2};
};
Helper Class::b{3};
int main() {}
Вывод будет таким:
Helper 1
Helper 2
Helper 3
В целом, картина довольно понятная. Если линкер ставит инициализацию статиков по порядку их определения, то здесь прослеживается та же история. Первыми инициализируются инлайны по порядку появления их в классе, а последним инициализируется неинлайновое поле, даже с учетом того, что оно объявлено между двумя первыми.
Так что порядок следования определений - наше все.
Rely on explicitly stated rules. Stay cool.
#cpp17 #cppcore