Как обмануть nodiscard?
В комментах к предыдущему посту Евгений правильно заметил, что аттрибут nodiscard можно заигнорировать. Правда непонятны кейсы, в которых это нужно делать и которые еще не притянутые были бы за уши. Думаю, что при корректном использовании атрибута, такой надобности не возникнет. Ну да ладно. Об этом мы поговорим попозже. Сейчас я перечислю некоторые способы обхода nodiscard, чисто из научного интереса. Предупреждаю сразу. Уберите маленьких детей от экрана и ни в коем случае не повторять дома. За последствия не отвечаю.
std::ignore. На этот вариант и ссылался Евгений. Суть в том, что этому безтиповому можно присвоить любое значение и не использовать его. Тогда и возвращаемое значение типа было использовано для преобразования в ignore, и мы потом этот ignore можем игнорировать. Подробнее тут. А для любителей покопаться в костях динозавров есть функция boost::ignore_unused.
Скастовать возвращаемое значение в void. Типа вот так: (void)someFunction(). Или более по-плюсовому co static_cast.
Присвоить возращаемое значение какому-то объекту. Но не использовать его.
Тогда появится варнинг, что переменная, которой мы присвоили возвращаемое значение, не используется нигде. А вот чтобы это обойти, нужно пометить эту переменную другим атрибутом [[maybe_unused]]. Например так: [[maybe_unused]] int i = foo ();
Сделать красивую шаблонную обертку над предыдущим пунктом, с variadic-templates и прочими радостями. И назвать ее discard.
Отличные новости для пользователей clang! Можно обернуть вызов функции в
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Weverything"
#endif
func_with_result();
#pragma clang diagnostic pop
#endif
Тогда и никаких варнингов генерироваться не будет. Для gcc есть что-то подобное, но там нельзя вроде все сразу отключить.
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wunused-result"
func_with_result();
#pragma GCC diagnostic pop
На этом моя фантазия кончилась. Но получилось все равно солидно)
Повторю, что в большинстве случаев вы этим будете стрелять себе в лицо, и это скорее всего признак того, что вы что-то делаете не так или система спроектирована плохо.
Возможно вы знаете какие-нибудь еще способы? Обязательно делитесь ими в комментариях)
Stay dangerous. Stay cool.
#fun #cpp17 #compiler
В комментах к предыдущему посту Евгений правильно заметил, что аттрибут nodiscard можно заигнорировать. Правда непонятны кейсы, в которых это нужно делать и которые еще не притянутые были бы за уши. Думаю, что при корректном использовании атрибута, такой надобности не возникнет. Ну да ладно. Об этом мы поговорим попозже. Сейчас я перечислю некоторые способы обхода nodiscard, чисто из научного интереса. Предупреждаю сразу. Уберите маленьких детей от экрана и ни в коем случае не повторять дома. За последствия не отвечаю.
std::ignore. На этот вариант и ссылался Евгений. Суть в том, что этому безтиповому можно присвоить любое значение и не использовать его. Тогда и возвращаемое значение типа было использовано для преобразования в ignore, и мы потом этот ignore можем игнорировать. Подробнее тут. А для любителей покопаться в костях динозавров есть функция boost::ignore_unused.
Скастовать возвращаемое значение в void. Типа вот так: (void)someFunction(). Или более по-плюсовому co static_cast.
Присвоить возращаемое значение какому-то объекту. Но не использовать его.
Тогда появится варнинг, что переменная, которой мы присвоили возвращаемое значение, не используется нигде. А вот чтобы это обойти, нужно пометить эту переменную другим атрибутом [[maybe_unused]]. Например так: [[maybe_unused]] int i = foo ();
Сделать красивую шаблонную обертку над предыдущим пунктом, с variadic-templates и прочими радостями. И назвать ее discard.
Отличные новости для пользователей clang! Можно обернуть вызов функции в
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Weverything"
#endif
func_with_result();
#pragma clang diagnostic pop
#endif
Тогда и никаких варнингов генерироваться не будет. Для gcc есть что-то подобное, но там нельзя вроде все сразу отключить.
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wunused-result"
func_with_result();
#pragma GCC diagnostic pop
На этом моя фантазия кончилась. Но получилось все равно солидно)
Повторю, что в большинстве случаев вы этим будете стрелять себе в лицо, и это скорее всего признак того, что вы что-то делаете не так или система спроектирована плохо.
Возможно вы знаете какие-нибудь еще способы? Обязательно делитесь ими в комментариях)
Stay dangerous. Stay cool.
#fun #cpp17 #compiler
🆒11👍7❤1🔥1
Линковка констант
Сегодня начнем затрагивать вопрос линковки переменных, линковки в целом и тонкости этого процесса. На эту тему несколько постов, плавно перетекающих друг в друга.
Давайте предположим, что у нас есть некоторый набор констант. Пусть это будут тривиальные физические константы, типа скорости света, числа авагадро и тд. И мы хотим использовать эти константы в разных единицах трансляции. Очевидный вариант - вынести их в какой-нибудь хэдер и подключать его всякий раз при необходимости. Получаем что-то типа такого:
Используем здесь constexpr для появления возможности использования этих констант в compile-time вычислениях.
Почему это вообще работает? Мы ведь здесь подключаем одно и то же определение в разные юниты трансляции. ODR должно нам запретить такое делать.
Дело в том, что все константы имеют по умолчанию внутреннее связывание. То же самое и для constexpr. Внутреннее связывание гарантирует, что в каждом юните трансляции будет использоваться своя копия этих переменных и ни из какого другого юнита нельзя будет получить доступ к ним. То есть определение этих констант везде будет свое. И ODR не будет нарушаться.
Если пометить константы как static, то ничего толком не изменится, потому что они и так неявно статические. То есть с внутренним связыванием.
У такого подхода есть проблемы.
Каждый раз, когда мы включаем заголовочный файл с константами в файл с кодом, каждая из этих переменных копируется в файл с кодом. Поэтому, если constants.hpp включается в 20 различных файлов кода, каждая из этих переменных дублируется 20 раз. Из этого следует следующее:
1️⃣ Изменение одной константы потребует перекомпиляции каждого файла, использующего константы(даже если измененная константа там не используется!), что делает компиляцию долгой для крупных проектов.
2️⃣ Если константы имеют большой размер и не могут быть оптимизированы, это приведёт к нежелательному расходу памяти.
Какое здесь решение? Подождать следующего поста, там будут объяснения)
Solve the problems. Stay cool.
#cppcore #compiler
Сегодня начнем затрагивать вопрос линковки переменных, линковки в целом и тонкости этого процесса. На эту тему несколько постов, плавно перетекающих друг в друга.
Давайте предположим, что у нас есть некоторый набор констант. Пусть это будут тривиальные физические константы, типа скорости света, числа авагадро и тд. И мы хотим использовать эти константы в разных единицах трансляции. Очевидный вариант - вынести их в какой-нибудь хэдер и подключать его всякий раз при необходимости. Получаем что-то типа такого:
// constants.hpp
#pragma once
namespace constants
{
constexpr unsigned light_speed { 299 792 458 };
constexpr double avogadro { 6.0221413e23 };
// ... other related constants
}
Используем здесь constexpr для появления возможности использования этих констант в compile-time вычислениях.
Почему это вообще работает? Мы ведь здесь подключаем одно и то же определение в разные юниты трансляции. ODR должно нам запретить такое делать.
Дело в том, что все константы имеют по умолчанию внутреннее связывание. То же самое и для constexpr. Внутреннее связывание гарантирует, что в каждом юните трансляции будет использоваться своя копия этих переменных и ни из какого другого юнита нельзя будет получить доступ к ним. То есть определение этих констант везде будет свое. И ODR не будет нарушаться.
Если пометить константы как static, то ничего толком не изменится, потому что они и так неявно статические. То есть с внутренним связыванием.
У такого подхода есть проблемы.
Каждый раз, когда мы включаем заголовочный файл с константами в файл с кодом, каждая из этих переменных копируется в файл с кодом. Поэтому, если constants.hpp включается в 20 различных файлов кода, каждая из этих переменных дублируется 20 раз. Из этого следует следующее:
1️⃣ Изменение одной константы потребует перекомпиляции каждого файла, использующего константы(даже если измененная константа там не используется!), что делает компиляцию долгой для крупных проектов.
2️⃣ Если константы имеют большой размер и не могут быть оптимизированы, это приведёт к нежелательному расходу памяти.
Какое здесь решение? Подождать следующего поста, там будут объяснения)
Solve the problems. Stay cool.
#cppcore #compiler
🔥15👍7❤5
Линковка констант Ч2
Мы узнали один из вариантов, как можно подключать константы в свои файлы с кодом. Однако у него были проблемы, которые мы попытаемся решить сегодня.
Все проблемы прошлого варианта по сути сводится к последствиям внутренней линковки.
Если у нас будет только одно определение переменной и весь остальной код будет только ссылаться на него, то решится проблема с перекомпиляцией. Потому что задача подстановки символов будет решаться при линковке. Во всех единицах трансляции будет просто заглушка для этой константы. И реальное значение будет подставляться компановщиком. А значит ничего не нужно заново компилировать.
Одно определение также решит вопрос нежелательного расхода памяти, так как экземпляр константы будет один и занимать одну условную единицу памяти. Никакого дублирования не будет.
Как мы можем добиться, чтобы определение констант было всего одно?
Обеспечить им внешнее связывание. С помощью ключевого слова extern.
Теперь константы будут создаваться только один раз (в единице трансляции соотвествующей constants.cpp), а не каждый раз при включении constants.h, и все использования будут просто ссылаться на версию в constants.cpp. Любые внесенные изменения в constants.cpp потребуют только перекомпиляции constants.cpp.
Однако и у этого метода есть несколько недостатков(да штож такое).
1️⃣ Эти константы теперь могут считаться константами времени компиляции только в файле, в котором они фактически определены (constants.cpp), а не где-либо еще. Это означает, что вне constants.cpp они не могут быть использованы нигде, где требуются вычисления в compile-time. Печально.
2️⃣ В принципе оптимизировать их использование компилятору сложнее, потому что он не имеет доступа к настоящему значению.
3️⃣ Неудобно просто. Каждый раз нужно ходить в реализацию, чтобы удостовериться в значении константы - такое себе. Да, современные IDE могут решить этот вопрос. А могут и не решить. Плюс нужно или мышку наводить или кнопки какие-то нажимать. Слишком много действий!
Шучу конечно. Но намного удобнее определение держать в хэдере.
Учитывая вышеперечисленные недостатки, хочется определять константы в заголовочном файле. Наконец-то мы подбираемся к самой мякотке. Но об этом - в следующих постах)
Stay in touch. Stay cool.
#cppcore #compiler
Мы узнали один из вариантов, как можно подключать константы в свои файлы с кодом. Однако у него были проблемы, которые мы попытаемся решить сегодня.
Все проблемы прошлого варианта по сути сводится к последствиям внутренней линковки.
Если у нас будет только одно определение переменной и весь остальной код будет только ссылаться на него, то решится проблема с перекомпиляцией. Потому что задача подстановки символов будет решаться при линковке. Во всех единицах трансляции будет просто заглушка для этой константы. И реальное значение будет подставляться компановщиком. А значит ничего не нужно заново компилировать.
Одно определение также решит вопрос нежелательного расхода памяти, так как экземпляр константы будет один и занимать одну условную единицу памяти. Никакого дублирования не будет.
Как мы можем добиться, чтобы определение констант было всего одно?
Обеспечить им внешнее связывание. С помощью ключевого слова extern.
//constant.cpp
#include "constant.hpp"
namespace constants
{
const unsigned light_speed { 299'792'458 };
const double avogadro { 6.0221413e23 };
// ... other related constants
}
//constant.hpp
#pragma once
namespace constants
{
extern const unsigned light_speed;
extern const double avogadro;
// ... other related constants
}
Теперь константы будут создаваться только один раз (в единице трансляции соотвествующей constants.cpp), а не каждый раз при включении constants.h, и все использования будут просто ссылаться на версию в constants.cpp. Любые внесенные изменения в constants.cpp потребуют только перекомпиляции constants.cpp.
Однако и у этого метода есть несколько недостатков(да штож такое).
1️⃣ Эти константы теперь могут считаться константами времени компиляции только в файле, в котором они фактически определены (constants.cpp), а не где-либо еще. Это означает, что вне constants.cpp они не могут быть использованы нигде, где требуются вычисления в compile-time. Печально.
2️⃣ В принципе оптимизировать их использование компилятору сложнее, потому что он не имеет доступа к настоящему значению.
3️⃣ Неудобно просто. Каждый раз нужно ходить в реализацию, чтобы удостовериться в значении константы - такое себе. Да, современные IDE могут решить этот вопрос. А могут и не решить. Плюс нужно или мышку наводить или кнопки какие-то нажимать. Слишком много действий!
Шучу конечно. Но намного удобнее определение держать в хэдере.
Учитывая вышеперечисленные недостатки, хочется определять константы в заголовочном файле. Наконец-то мы подбираемся к самой мякотке. Но об этом - в следующих постах)
Stay in touch. Stay cool.
#cppcore #compiler
👍15❤5🔥5
Когда НЕ стоит использовать extern template
Если вы будете гуглить инфу по этой теме, то непременно нарветесь на неправильное понимание принципов работы фичи. Стопроцентов вы наткнетесь на такое объяснение:
extern template в связке с явным инстанцированием шаблона помогает предотвратить дублирование кода в TU и уменьшить время компиляции. Выглядит это так:
Типа вот мы в ship.cpp добавили явную конкретизацию шаблона, а в мэйне объявили, что возьмем информацию о специализации в другом месте.
Но дело в том, что в этом случае extern template - лишний! В нем нет никакого смысла и вот почему.
Если мы уберем extern template из файла main, то ничего не изменится. Так как в этой единице трансляции и так никогда бы не была конкретизирована специализация Ship<int>. Потому что компилятору на момент компиляции файла main.cpp видно только объявление шаблона из файла ship.hpp и у него недостаточно информации для инстанцирования. И только при линковке линковщик найдет все символы в единице трансляции, соответствующей ship.cpp, и сгенерирует рабочую программу.
Так что запомните: если вы используете явную инстанциацию после определения шаблона в цппшнике и подключаете хэдэр с его объявлением, то вам НЕ НУЖНО использовать extern template.
Это кстати отличная защита от тех самых проблем при работе с шаблонами. Так что выносить определенения шаблонов в цппшники и делать в них явную инстанциацию - полезная вещь.
Также без подключения хэдэра эта вещь вообще не работает, в отличии например от глобальных переменных. Все-таки контекст extern здесь не совсем совпадает.
А вот когда это нужно использовать. Только тогда, когда у вас есть несколько единиц трансляции, где компилятор сам неявно может инстанцировать одинаковые специализации. Например, когда вы полностью определяете шаблон в хэдэре и везде его распространяете таким образом. Тогда получается, что без использования extern template в каждой из этих единиц трансляций, подключивших хэдэр с шаблоном и использующих одинаковую специализацию, эта специализация будет инстанцирована. Это значит, что код для нее будет присутствовать во всех объектниках. Это приводит к его дублированию и увеличению времени компиляции.
Теперь мы во всех TU, кроме одной, используем extern template и в этой оставшейся делаем явную специализацию. Получается, что для всех, кроме одной, TU компилятору будет запрещено самостоятельно инстанцировать эту специализацию. И все они будет обращаться в тот единственный объектник, в котором есть код для специализации. Именно за счет этого и не происходит раздувания итогового бинарника. Все просто полагаются на одну копию.
Rely on original information. Stay cool.
#cpp11 #cppcore #template #compiler
Если вы будете гуглить инфу по этой теме, то непременно нарветесь на неправильное понимание принципов работы фичи. Стопроцентов вы наткнетесь на такое объяснение:
extern template в связке с явным инстанцированием шаблона помогает предотвратить дублирование кода в TU и уменьшить время компиляции. Выглядит это так:
// ship.hpp
#pragma once
template<typename T>
struct Ship
{
void TurnShip(T command);
};
// ship.cpp
#include "ship.hpp"
template <class T>
void Ship<T>::TurnShip(T command) {}
template struct Ship<int>; // explicit instantiation definition
// main.cpp
#include "ship.hpp"
extern template struct Ship<int>; // explicit instantiation declaration
int main() {
Ship<int> ship;
ship.TurnShip(5);
}
Типа вот мы в ship.cpp добавили явную конкретизацию шаблона, а в мэйне объявили, что возьмем информацию о специализации в другом месте.
Но дело в том, что в этом случае extern template - лишний! В нем нет никакого смысла и вот почему.
Если мы уберем extern template из файла main, то ничего не изменится. Так как в этой единице трансляции и так никогда бы не была конкретизирована специализация Ship<int>. Потому что компилятору на момент компиляции файла main.cpp видно только объявление шаблона из файла ship.hpp и у него недостаточно информации для инстанцирования. И только при линковке линковщик найдет все символы в единице трансляции, соответствующей ship.cpp, и сгенерирует рабочую программу.
Так что запомните: если вы используете явную инстанциацию после определения шаблона в цппшнике и подключаете хэдэр с его объявлением, то вам НЕ НУЖНО использовать extern template.
Это кстати отличная защита от тех самых проблем при работе с шаблонами. Так что выносить определенения шаблонов в цппшники и делать в них явную инстанциацию - полезная вещь.
Также без подключения хэдэра эта вещь вообще не работает, в отличии например от глобальных переменных. Все-таки контекст extern здесь не совсем совпадает.
А вот когда это нужно использовать. Только тогда, когда у вас есть несколько единиц трансляции, где компилятор сам неявно может инстанцировать одинаковые специализации. Например, когда вы полностью определяете шаблон в хэдэре и везде его распространяете таким образом. Тогда получается, что без использования extern template в каждой из этих единиц трансляций, подключивших хэдэр с шаблоном и использующих одинаковую специализацию, эта специализация будет инстанцирована. Это значит, что код для нее будет присутствовать во всех объектниках. Это приводит к его дублированию и увеличению времени компиляции.
Теперь мы во всех TU, кроме одной, используем extern template и в этой оставшейся делаем явную специализацию. Получается, что для всех, кроме одной, TU компилятору будет запрещено самостоятельно инстанцировать эту специализацию. И все они будет обращаться в тот единственный объектник, в котором есть код для специализации. Именно за счет этого и не происходит раздувания итогового бинарника. Все просто полагаются на одну копию.
Rely on original information. Stay cool.
#cpp11 #cppcore #template #compiler
👍18❤4🔥2👎1
Когда стоит использовать explicit template declaration
Мы поговорили о случае, в котором бесполезно использовать explicit template declaration. Теперь поговорим о наиболее уместном и логичном способе использования этой фичи.
Главная функция extern template - запретить компилятору неявную инстанциацию. Значит, для адекватного использования этой конструкции компилятору необходимо иметь возможность выполнить эту неявную инстанциацию. Единственным подходящим ситуации вариантом здесь будет нахождение полного определения шаблона в хэдэре, чтобы его могли видеть все заинтересованные лица(пофантизируйте в комментариях, как могло бы выглядеть лицо у единицы трансляции).
Дальше есть следующие 2 варианта - поместить все явные объявления инстанциации шаблона в этот же хэдэр и распихать по единицам трансляции. Как по мне, лучше иметь одну централизированную точку изменений, так как программисты - люди забывчивые и могут упустить момент добавления нового явного объявления и компилятор сам сделает неявную инстанциацию. Да и если помещать в разные места, то это приведет к дубликации кода. Поэтому оставляем extern template в хэдэре.
Ну и последний момент. Если есть явное объявление инстанциации, должно быть и ее явное определение. Причем это ВАЖНО. Нельзя при использовании extern template полагаться на неявную инстанциацию. В нашем случае это уже невозможно, потому что мы добавили в хэдэр с шаблоном запрет на неявную инстанциацию, но я все равно хочу на это обратить ваше внимание. Компилятор может ее оптимизировать, так что для нее больше не останется отдельно скомпилированной сущности и все вызовы просто встроятся. Тогда компановщик не сможет разрезолвить символы и будет undefined reference. Чуть позже расскажу об этом в отдельном посте.
Итак, explicit template instantiation. Мы помещаем явные определения всех нужных нам неявных специализаций в отдельный цппшник. И вот к коду в этой TU будет обращаться линкер, чтобы подставить адреса нужных вызовов. А в других TU не будет сгенерировано ничего связанного с шаблоном.
Продемонстрирую на примере:
Если мы отдельно скомпилируем main.cpp и посмотрим на символы объектника, то там будет только то, что связано с std::basic_string, но не с Ship. Как и было задумано.
Подводя итог: нам нужен хэдэр с полным определением шаблона и явными объявлениями extern template и сорец с явными определениями этих инстанциаций. Теперь мы можем везде тыкать наш хэдэр и ожидать уменьшения времени компиляции и меньшего размера объектников.
Choose the right way. Stay cool.
#template #compiler #cppcore
Мы поговорили о случае, в котором бесполезно использовать explicit template declaration. Теперь поговорим о наиболее уместном и логичном способе использования этой фичи.
Главная функция extern template - запретить компилятору неявную инстанциацию. Значит, для адекватного использования этой конструкции компилятору необходимо иметь возможность выполнить эту неявную инстанциацию. Единственным подходящим ситуации вариантом здесь будет нахождение полного определения шаблона в хэдэре, чтобы его могли видеть все заинтересованные лица(пофантизируйте в комментариях, как могло бы выглядеть лицо у единицы трансляции).
Дальше есть следующие 2 варианта - поместить все явные объявления инстанциации шаблона в этот же хэдэр и распихать по единицам трансляции. Как по мне, лучше иметь одну централизированную точку изменений, так как программисты - люди забывчивые и могут упустить момент добавления нового явного объявления и компилятор сам сделает неявную инстанциацию. Да и если помещать в разные места, то это приведет к дубликации кода. Поэтому оставляем extern template в хэдэре.
Ну и последний момент. Если есть явное объявление инстанциации, должно быть и ее явное определение. Причем это ВАЖНО. Нельзя при использовании extern template полагаться на неявную инстанциацию. В нашем случае это уже невозможно, потому что мы добавили в хэдэр с шаблоном запрет на неявную инстанциацию, но я все равно хочу на это обратить ваше внимание. Компилятор может ее оптимизировать, так что для нее больше не останется отдельно скомпилированной сущности и все вызовы просто встроятся. Тогда компановщик не сможет разрезолвить символы и будет undefined reference. Чуть позже расскажу об этом в отдельном посте.
Итак, explicit template instantiation. Мы помещаем явные определения всех нужных нам неявных специализаций в отдельный цппшник. И вот к коду в этой TU будет обращаться линкер, чтобы подставить адреса нужных вызовов. А в других TU не будет сгенерировано ничего связанного с шаблоном.
Продемонстрирую на примере:
// ship.hpp
#pragma once
#include <string>
template<typename T>
struct Ship
{
// contain some fields
void TurnShip(T command);
};
template <class T>
void Ship<T>::TurnShip(T command) {/* do stuff using command */}
extern template class Ship<std::string>; // text command
extern template class Ship<int>; // turn certain number of degrees clockwise
// ship.cpp
#include "ship.hpp"
template class Ship<std::string>;
template class Ship<int>;
// main.cpp
#include "ship.hpp"
#include <string>
int main() {
Ship<std::string> ship;
ship.TurnShip(std::string{"Turn upside down"});
Ship<int> ship1; // i know it's silly to instantiate 2 version of
// ship just to have a different style of turning,
// but stick to the goodold example
ship1.TurnShip(36'000); // just trying to make a giant whirlpool
}
Если мы отдельно скомпилируем main.cpp и посмотрим на символы объектника, то там будет только то, что связано с std::basic_string, но не с Ship. Как и было задумано.
Подводя итог: нам нужен хэдэр с полным определением шаблона и явными объявлениями extern template и сорец с явными определениями этих инстанциаций. Теперь мы можем везде тыкать наш хэдэр и ожидать уменьшения времени компиляции и меньшего размера объектников.
Choose the right way. Stay cool.
#template #compiler #cppcore
👍21❤9🔥3
Встраивание шаблонов
Небольшое предисловие: один из админов и автор последующих 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
👍17❤6🔥4👏1
Не всегда инстанциация шаблона нужна для работы программы
Возьмем пример из прошлого поста, объединим в хэдэре объявление шаблона с его определением и выкинем 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
🔥10👍5❤2
Линкуем массивы к объектам
Опытные читатели могли заметить кое-что странное в этом посте. И заметили кстати. Изначально cin, cout и тд определены, как простые массивы. А в iostream они уже становятся объектами потоков и линкуются как онные. То есть в одной единице трансляции
А в другой
Что за приколы такие? Почему массивы нормально линкуются на объекты кастомных классов?
В С++ кстати запрещены такие фокусы. Типы объявления и определения сущности должны совпадать.
Все потому что линкер особо не заботится о типах, выравнивании и даже особо о размерах объектов. То есть я буквально могу прилинковать объект одного кастомного класса к другому и мне никто никакого предупреждения не влепит. Такой код вполне нормально компилится и запускается:
На консоли появится "1 2". Но ни типы, ни размеры типов, ни выравнивания у объектов из объявления и определения не совпадают. Поэтому здесь явное UB.
Но в исходниках GCC так удачно сложилось, что массивы реально представляют собой идеальные сосуды для объектов io-потоков. На них даже сконструировали реальные объекты. Поэтому такие массивы можно интерпретировать как сами объекты.
Это, естественно, все непереносимо. Но поговорка "спички детям - не игрушка" подходит только для тех, кто плохо понимает, что делает. А разработчики компилятора явно не из этих ребят.
Take conscious risks. Stay cool.
#cppcore #compiler
Опытные читатели могли заметить кое-что странное в этом посте. И заметили кстати. Изначально cin, cout и тд определены, как простые массивы. А в iostream они уже становятся объектами потоков и линкуются как онные. То есть в одной единице трансляции
extern std::ostream cout;
extern std::istream cin;
...
А в другой
// Standard stream objects.
// NB: Iff <iostream> is included, these definitions become wonky.
typedef char fake_istream[sizeof(istream)]
attribute ((aligned(alignof(istream))));
typedef char fake_ostream[sizeof(ostream)]
attribute ((aligned(alignof(ostream))));
fake_istream cin;
fake_ostream cout;
fake_ostream cerr;
fake_ostream clog;
Что за приколы такие? Почему массивы нормально линкуются на объекты кастомных классов?
В С++ кстати запрещены такие фокусы. Типы объявления и определения сущности должны совпадать.
Все потому что линкер особо не заботится о типах, выравнивании и даже особо о размерах объектов. То есть я буквально могу прилинковать объект одного кастомного класса к другому и мне никто никакого предупреждения не влепит. Такой код вполне нормально компилится и запускается:
// header.hpp
#pragma once
struct TwoFields {
int a;
int b;
};
struct ThreeFields {
char a;
int b;
long long c;
};
// source.cpp
ThreeFields test = {1, 2, 3};
// main.cpp
#include <iostream>
#include "header.hpp"
extern TwoFields test;
int main() {
std::cout << test.a << " " << test.b << std::endl;
}
На консоли появится "1 2". Но ни типы, ни размеры типов, ни выравнивания у объектов из объявления и определения не совпадают. Поэтому здесь явное UB.
Но в исходниках GCC так удачно сложилось, что массивы реально представляют собой идеальные сосуды для объектов io-потоков. На них даже сконструировали реальные объекты. Поэтому такие массивы можно интерпретировать как сами объекты.
Это, естественно, все непереносимо. Но поговорка "спички детям - не игрушка" подходит только для тех, кто плохо понимает, что делает. А разработчики компилятора явно не из этих ребят.
Take conscious risks. Stay cool.
#cppcore #compiler
🔥47🤯10❤🔥4👍4❤2
#include <filename> vs #include "filename"
#новичкам
Тот нюанс, который зеленые программисты С++ впитывают из окружающей среды, но зачастую не понимают его деталей.
Дано: файл, который нужно включить в проект. Задача: определить, включать его через треугольные скобки или кавычки. Какой подход выбрать?
Стандарт нам дает отписку, что поведение при обоих подходах implementation defined, поэтому надо смотреть на то, как ведут себя большинство компиляторов.
Для начала: #include - это директива препроцессора. На месте этой директивы на этапе препроцессинга вставляется тело включаемого файла. Но для того, чтобы вставить текст файла, его надо в начале найти. И вот здесь главное различие.
У компилятора есть 2 вида путей, где он ищет файлы - системные директории и предоставленные юзером через опцию компиляции -I.
Так вот #include <filename> в начале ищет файл в системных директориях. Например, в линуксе хэдэры устанавливаемых библиотек могут оказаться в директориях /usr/include/, /usr/local/include/ или /usr/include/x86_64-linux-gnu/(на x86 системах).
А #include "filename" в начале ищет файл в текущей директории и в директориях, предоставленных через опцию компиляции.
В конце концов, обычно, в обоих случаях все известные компилятору директории будут просмотрены на наличие подходящего файла, пока он не будет найден. Вопрос только в порядке поиска.
Так что в большинстве случаев разницы особо никакой не будет, кроме времени компиляции. Однако все равно есть определенные гайдлайны, которым следуют большинство разработчиков.
✅ Используем #include <filename> для включения стандартных файлов и хэдэров сторонних библиотек. Так как они скорее всего установлены по стандартным директориям, логично именно там начинать их поиск.
✅ Используем #include "filename" для включения заголовочных файлов своего проекта. Препроцессор будет сначала искать эти файлы там, где вы ему об этом явно укажите с помощью опций.
See the difference. Stay cool.
#cppcore #compiler
#новичкам
Тот нюанс, который зеленые программисты С++ впитывают из окружающей среды, но зачастую не понимают его деталей.
Дано: файл, который нужно включить в проект. Задача: определить, включать его через треугольные скобки или кавычки. Какой подход выбрать?
Стандарт нам дает отписку, что поведение при обоих подходах implementation defined, поэтому надо смотреть на то, как ведут себя большинство компиляторов.
Для начала: #include - это директива препроцессора. На месте этой директивы на этапе препроцессинга вставляется тело включаемого файла. Но для того, чтобы вставить текст файла, его надо в начале найти. И вот здесь главное различие.
У компилятора есть 2 вида путей, где он ищет файлы - системные директории и предоставленные юзером через опцию компиляции -I.
Так вот #include <filename> в начале ищет файл в системных директориях. Например, в линуксе хэдэры устанавливаемых библиотек могут оказаться в директориях /usr/include/, /usr/local/include/ или /usr/include/x86_64-linux-gnu/(на x86 системах).
А #include "filename" в начале ищет файл в текущей директории и в директориях, предоставленных через опцию компиляции.
В конце концов, обычно, в обоих случаях все известные компилятору директории будут просмотрены на наличие подходящего файла, пока он не будет найден. Вопрос только в порядке поиска.
Так что в большинстве случаев разницы особо никакой не будет, кроме времени компиляции. Однако все равно есть определенные гайдлайны, которым следуют большинство разработчиков.
✅ Используем #include <filename> для включения стандартных файлов и хэдэров сторонних библиотек. Так как они скорее всего установлены по стандартным директориям, логично именно там начинать их поиск.
#include <stdio.h> // Стандартный заголовочник
#include <curl/curl.h> // Заголовочник из системной директории
int main(void) {
CURL *curl = curl_easy_init();
if(curl) {
printf("libcurl version: %s\n", curl_version());
curl_easy_cleanup(curl);
}
return 0;
}
✅ Используем #include "filename" для включения заголовочных файлов своего проекта. Препроцессор будет сначала искать эти файлы там, где вы ему об этом явно укажите с помощью опций.
// include/mylib.hpp - объявляем функцию из нашего проекта
#pragma once
void print_hello();
// src/main.cpp - используем локальный заголовочник через " "
#include <iostream> // Системный заголовочник
#include "mylib.hpp" // Заголовочник локального проекта, ищем в указанных путях
int main() {
print_hello();
return 0;
}
void print_hello() {
std::cout << "Hello from my project!\n";
}
// компиляция: g++ -Iinclude/ src/main.cpp -o my_program -std=c++17
See the difference. Stay cool.
#cppcore #compiler
1❤61👍25🔥9
Unity build
#опытным
Чем знаменит С++? Конечно же своим гигантским временем сборки программ. Пока билдится плюсовый билд, где-то в Китае строится новый небоскреб.
Конечно это бесит всех в коммьюнити и все пытаются сократить время ожидания сборки. Для этого есть несколько подходов, один из которых мы обсудим сегодня.
Компиляция всяких шаблонов сама по себе долгая, особенно, если использовать какие-нибудь рэнджи или std::format. Но помните, что конкретная инстанциация шаблона будет компилироваться независимо в каждой единице трансляции. В одном цппшнике использовали
Но помимо компиляции вообще-то есть линковка. И чем больше единиц трансляции, библиотек и все прочего, тем больше времени нужно линковщику на соединение все этого добра в одно целое.
Обе эти проблемы можно решить одним махом - просто берем и подключаем все цппшники в один большооой и главный цппшник. И компилируем только его. Такой себе один большой main. Такая техника называется Unity build (aka jumbo build или blob build)
Условно. Есть у вас 2 цппшника и один хэдэр:
Вы все цппшники подключаете в один файл unity_build.cpp:
И компилируете его. За счет гардов хэдэров у вас будет по одной версии каждого из них в едином файле, меньше кода анализируется и компилируется в принципе. Каждая инстанциация шаблона компилируется ровно однажды, а затраты на линковку отсутствуют. Красота!
Или нет?
У этой техники есть ряд недостатков:
Потеря преимуществ инкрементной сборки. При изменении даже одного маленького файла приходится перекомпилировать всю объединенную единицу трансляции, что значительно увеличивает время и именно пересборки. Сборка быстрее, но пересборка потенциально медленнее.
Потенциальные конфликты имен. Конфликты статических переменных и функций с одинаковыми именами в разных файлах, конфликты символов из анонимных namespace'ов, неожиданное разрешение перегрузки функций - все это может подпортить вам жизнь.
Сложность отладки. Вас ждут увлекательные ошибки компиляции и нетривиальная навигация по ним.
У кого был опыт с unity билдами, отпишитесь по вашим впечатлениям.
Solve the problem. Stay cool.
#cppcore #compiler #tools
#опытным
Чем знаменит С++? Конечно же своим гигантским временем сборки программ. Пока билдится плюсовый билд, где-то в Китае строится новый небоскреб.
Конечно это бесит всех в коммьюнити и все пытаются сократить время ожидания сборки. Для этого есть несколько подходов, один из которых мы обсудим сегодня.
Компиляция всяких шаблонов сама по себе долгая, особенно, если использовать какие-нибудь рэнджи или std::format. Но помните, что конкретная инстанциация шаблона будет компилироваться независимо в каждой единице трансляции. В одном цппшнике использовали
std::vector<int> - компилируем эту инстанциацию. В другом написали std::vector<int> - заново скомпилировали эту инстанциацию. То есть большая проблема в компиляции одного и того же кучу раз.Но помимо компиляции вообще-то есть линковка. И чем больше единиц трансляции, библиотек и все прочего, тем больше времени нужно линковщику на соединение все этого добра в одно целое.
Обе эти проблемы можно решить одним махом - просто берем и подключаем все цппшники в один большооой и главный цппшник. И компилируем только его. Такой себе один большой main. Такая техника называется Unity build (aka jumbo build или blob build)
Условно. Есть у вас 2 цппшника и один хэдэр:
// header.hpp
#pragma once
void foo();
// source1.cpp
#include "header.hpp"
void foo() {
std::cout << "You are the best!" << std::endl;
}
// source2.cpp
#include "header.hpp"
int main() {
foo();
}
Вы все цппшники подключаете в один файл unity_build.cpp:
#include "source1.cpp"
#include "source2.cpp"
И компилируете его. За счет гардов хэдэров у вас будет по одной версии каждого из них в едином файле, меньше кода анализируется и компилируется в принципе. Каждая инстанциация шаблона компилируется ровно однажды, а затраты на линковку отсутствуют. Красота!
Или нет?
У этой техники есть ряд недостатков:
Потеря преимуществ инкрементной сборки. При изменении даже одного маленького файла приходится перекомпилировать всю объединенную единицу трансляции, что значительно увеличивает время и именно пересборки. Сборка быстрее, но пересборка потенциально медленнее.
Потенциальные конфликты имен. Конфликты статических переменных и функций с одинаковыми именами в разных файлах, конфликты символов из анонимных namespace'ов, неожиданное разрешение перегрузки функций - все это может подпортить вам жизнь.
Сложность отладки. Вас ждут увлекательные ошибки компиляции и нетривиальная навигация по ним.
У кого был опыт с unity билдами, отпишитесь по вашим впечатлениям.
Solve the problem. Stay cool.
#cppcore #compiler #tools
1❤23🔥7👍6😁2🗿1