Квиз
Сегодня будет совсем простенький квиз, предвосхищающий раскрытие очень занудной, но важной темы. Ответ скину, по-классике, вечером.
Какой будет вывод у этого кода?
Сегодня будет совсем простенький квиз, предвосхищающий раскрытие очень занудной, но важной темы. Ответ скину, по-классике, вечером.
#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.
Встраивание шаблонов
Небольшое предисловие: один из админов и автор последующих 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
Квиз
Возвращаемся в рабочие будни с интересным(надеюсь) #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;
}
Линковка 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
bit_cast
Начиная с C++20 появилась шаблонная функция
В конкретном примере переменные
Аналогичного результата можно добиться и с помощью
В отличие от альтернативных способов, шаблонная функция
Продемонстрирую проблему на примере с
Бывает и так, что изначально некоторые типы были реализованы тривиальными, но затем (в ходе доработок) потеряли такое свойство. Встроенные проверки
Нельзя назвать
Оставляйте реакции, считаете ли вы этот пост полезным для других! А мы, как и всегда, будем рады прочитать ваши комментарии и ответить вопросы 😉
#cppcore #cpp20
Начиная с C++20 появилась шаблонная функция
std::bit_cast
в заголовочном файле <bit>
. Она предоставляет возможность создавать побитовые копии объектов с другим типом:#include <bit>
double src = 42.0;
uint64_t dst = std::bit_cast<uint64_t>(src);
В конкретном примере переменные
dst
и src
имеют одинаковый размер 8 байт, поэтому их содержимое может быть интерпретировано по-разному, в зависимости от типа представления: беззнаковое целое или число с плавающей запятой.Аналогичного результата можно добиться и с помощью
union
или reinterpret_cast
. Однако, это нельзя было сделать в compile time! Функция std::bit_cast
поддерживает constexpr
выражения. Бонусом мы получаем достаточно лаконичное приведение и не нарушаем strict aliasing.В отличие от альтернативных способов, шаблонная функция
std::bit_cast
дополнительно проверяет, что исходный и целевой типы имеют одинаковый размер и могут быть тривиально скопированы. Последнее означает, что память объекта может быть просто скопирована без дополнительных действий и это будет рабочей копией. Если это не так, то такие операции могут нарушать жизненный цикл нового объекта.Продемонстрирую проблему на примере с
std::string
. Объекты данного типа, в общем случае, хранят строку где-то в другом месте, а сами выступают в роли умной оболочки (RAII). Клонирование такого объекта «в лоб» создает потенциально опасную ситуацию: два объекта будут ссылаться на один и тот же уникальный ресурс и пытаться управлять им. Например, они оба попытаются освободить ресурс. У первого объекта это получится, а у второго приведет к ошибке double free: живой пример. Отсюда и вытекает ограничение, что нельзя создавать побитовых клонов нетривиально копируемых объектов. Им необходимо обязательно вызвать конструктор копирования, который выделит собственный ресурс. Бывает и так, что изначально некоторые типы были реализованы тривиальными, но затем (в ходе доработок) потеряли такое свойство. Встроенные проверки
std::bit_cast
тут же сообщат о некорректности работы с таким типом.Нельзя назвать
std::bit_cast
оператором приведения, т.к. эта штука все таки не включена в семантику языка (в отличие от static_cast
, reinterpret_cast
и т.д.) и вынесена в пространство имен библиотеки STL. Однако её стоит упомянуть в текущем цикле статей.Оставляйте реакции, считаете ли вы этот пост полезным для других! А мы, как и всегда, будем рады прочитать ваши комментарии и ответить вопросы 😉
#cppcore #cpp20
Инициализация статических полей класса. Ч4
Продолжение нелегендарной истории static class members initialization. Предыдущие части тут, тут и тут.
Я немного наврал, когда сказал, что статические переменные и мемберы инициализируются до входа в main(). Как Эдгар отметил в своем комменте, на самом деле тут вот что:
Стандарт дает на откуп реализациям вопрос о том, в какой конкретно момент времени происходит динамическая инициализация глобальных объектов. Единственное ограничение, что инициализация должна произойти до любого неинициализирующего odr-use действия над неинлайн переменными и функциями, определенными в той же единице трансляции, где переменная собирается инициализироваться(немного духоты). То есть до любого действия по считыванию, записи, взятию адреса и созданию ссылки от переменной или функции.
Довольно сложно воспроизвести пример, когда инициализация происходит после main(), потому что мы на это напрямую не может повлиять. Поэтому может быть вот такой потенциальный пример. Он не про статические поля класса конкретно, но зато более наглядный.
В мейне мы говорим, что где-то определен массив интов и внутри главной функции мы печатаем его содержимое.
Проблема в том, что не понятно, произойдет ли в source.cpp инициализация array до вызова main() или после. Если после, то мы вполне можем накнуться на неинициализированную память, что UB.
Подливает масло в огонь вот такое утверждение:
То есть если никакие переменные и функции не используются в юните, где переменная должна инициализироваться, ее значение вообще может быть не установлено.
Так и происходит в source.cpp. Что с бо'льшей вероятностью приведет эту программу к фрилансерскому(нерабочему) состоянию. Однако популярные компиляторы стараются сгладить углы в этом моменте и даже в таком виде у вас в 99.9 случаев из 100 будет все в порядке. Что не отменяет потенциальную угрозу, но тем не менее.
Такие проблемы скорее свойственны программам, использующим шареные библиотеки, потому что они сами по себе в рантайме подгружаются и непонятно когда это происходит. Да и стандарт никак не упоминает эти библиотеки, поэтому здесь большой простор для фантазии и поведения реализации.
Avoid dangerous situations with no gain. Stay cool.
#cppcore
Продолжение нелегендарной истории static class members initialization. Предыдущие части тут, тут и тут.
Я немного наврал, когда сказал, что статические переменные и мемберы инициализируются до входа в main(). Как Эдгар отметил в своем комменте, на самом деле тут вот что:
It is implementation-defined whether
the dynamic initialization of
a non-block non-inline variable with
static storage duration is sequenced
before the first statement of main or
is deferred. If it is deferred, it
strongly happens before any
non-initialization odr-use of any
non-inline function or non-inline
variable defined in the same
translation unit as the variable to be
initialized. It is implementation-defined
in which threads and at which points in
the program such deferred dynamic
initialization occurs.
Стандарт дает на откуп реализациям вопрос о том, в какой конкретно момент времени происходит динамическая инициализация глобальных объектов. Единственное ограничение, что инициализация должна произойти до любого неинициализирующего odr-use действия над неинлайн переменными и функциями, определенными в той же единице трансляции, где переменная собирается инициализироваться(немного духоты). То есть до любого действия по считыванию, записи, взятию адреса и созданию ссылки от переменной или функции.
Довольно сложно воспроизвести пример, когда инициализация происходит после main(), потому что мы на это напрямую не может повлиять. Поэтому может быть вот такой потенциальный пример. Он не про статические поля класса конкретно, но зато более наглядный.
// header.hpp
struct Class {
Class() : array{1, 2, 3, -1} {}
int array[4];
};
//source.cpp
#include "header.hpp"
Class var;
// main.cpp
#include <cstdio>
#include "header.hpp"
extern Class var;
int main(void)
{
for (int i = 0; var.array[i] != -1; i++) {
printf("%d\n", i);
}
}
В мейне мы говорим, что где-то определен массив интов и внутри главной функции мы печатаем его содержимое.
Проблема в том, что не понятно, произойдет ли в source.cpp инициализация array до вызова main() или после. Если после, то мы вполне можем накнуться на неинициализированную память, что UB.
Подливает масло в огонь вот такое утверждение:
If no variable or function is odr-used
from a given translation unit, the
non-local variables defined in that
translation unit may never be initialized
То есть если никакие переменные и функции не используются в юните, где переменная должна инициализироваться, ее значение вообще может быть не установлено.
Так и происходит в source.cpp. Что с бо'льшей вероятностью приведет эту программу к фрилансерскому(нерабочему) состоянию. Однако популярные компиляторы стараются сгладить углы в этом моменте и даже в таком виде у вас в 99.9 случаев из 100 будет все в порядке. Что не отменяет потенциальную угрозу, но тем не менее.
Такие проблемы скорее свойственны программам, использующим шареные библиотеки, потому что они сами по себе в рантайме подгружаются и непонятно когда это происходит. Да и стандарт никак не упоминает эти библиотеки, поэтому здесь большой простор для фантазии и поведения реализации.
Avoid dangerous situations with no gain. Stay cool.
#cppcore