Нужны ли стражи включения в C++
28.10.2024
|
velkin |
Читал я тут очередную книгу по C++ и мне внезапно подумалось, а нужны ли стражи включения.
Типичный страж включения на основе макросов.
Или директивой, которую в наше время уже можно считать стандартной.
Но у меня годами вертелась мысль про управление лишними #include. Пишешь какой-нибудь код и добавляешь #include, а потом ещё и ещё. А потом стираешь функционал, а #include остаётся.
Но что ещё важнее и относится как раз к стражу включения, можно бесконечно дублировать #include для разных и одинаковых файлов. Когда есть вложения, то этого не видно, но я могу сделать и так.
Мало того, что функционал этих включений нигде не применяется, но они ещё и продублированы много раз.
В противовес можно было бы использовать ручное управление иерархией вложений собственных включений. Если бы чужие библиотеки не использовали стражи включения, тогда пришлось бы учитывать при включении файлов иерархии чужих библиотек.
И вот казалось бы, а зачем, это же лишние заморочки. А с другой стороны отсутствие ручного управления иерархиями включений файлов это и есть отказ от управления иерархиями.
Плюсы.
1. Меньше возни с иерархиями включений файлов.
Минусы.
1. Программист без понятия об иерархиях включения файлов, то есть часть взаимосвязей в коде не улавливает.
2. Код со стражами включений менее красивый и более отвлекающий, особенно когда используется конструкция из макросов.
А кто там говорил, что макросы в принципе не нужно использовать, не Страуструп ли Бьерн, создатель языка. Я просто уже так привык к стражам включения и что визарды мне с ходу эти штуки генерирует, что перестал замечать. А ведь по большому счёту их можно и не использовать.
И опять же кто что думает по этому поводу. Но не так, что всю жизнь использовали, а более осмысленное. Мне лично стражи включения навязали визарды. Меня даже никто не спрашивал, потому я особо и не задумывался над целесообразностью.
Типичный страж включения на основе макросов.
#ifndef NAME
#define NAME
// ...
#endif // NAME
Или директивой, которую в наше время уже можно считать стандартной.
#pragma once
Но у меня годами вертелась мысль про управление лишними #include. Пишешь какой-нибудь код и добавляешь #include, а потом ещё и ещё. А потом стираешь функционал, а #include остаётся.
Но что ещё важнее и относится как раз к стражу включения, можно бесконечно дублировать #include для разных и одинаковых файлов. Когда есть вложения, то этого не видно, но я могу сделать и так.
#include <iostream>
#include <iostream>
#include <iostream>
#include <vector>
#include <vector>
#include <vector>
int main()
{
return 0;
}
Мало того, что функционал этих включений нигде не применяется, но они ещё и продублированы много раз.
В противовес можно было бы использовать ручное управление иерархией вложений собственных включений. Если бы чужие библиотеки не использовали стражи включения, тогда пришлось бы учитывать при включении файлов иерархии чужих библиотек.
И вот казалось бы, а зачем, это же лишние заморочки. А с другой стороны отсутствие ручного управления иерархиями включений файлов это и есть отказ от управления иерархиями.
Плюсы.
1. Меньше возни с иерархиями включений файлов.
Минусы.
1. Программист без понятия об иерархиях включения файлов, то есть часть взаимосвязей в коде не улавливает.
2. Код со стражами включений менее красивый и более отвлекающий, особенно когда используется конструкция из макросов.
А кто там говорил, что макросы в принципе не нужно использовать, не Страуструп ли Бьерн, создатель языка. Я просто уже так привык к стражам включения и что визарды мне с ходу эти штуки генерирует, что перестал замечать. А ведь по большому счёту их можно и не использовать.
И опять же кто что думает по этому поводу. Но не так, что всю жизнь использовали, а более осмысленное. Мне лично стражи включения навязали визарды. Меня даже никто не спрашивал, потому я особо и не задумывался над целесообразностью.
28.10.2024 31 комментарий |
ВР>https://rsdn.org/forum/cpp/8169360
Там обсуждают использовать ли макросы или директиву в конструкции стража включения, или всё вместе. А я подумал о том, что зачем это не сделали по умолчанию опцией компилятора, то есть автоматическим стражем включения для каждого файла.
Для чего вообще включать два или более раз одно и тоже, что явно вызовет ошибку компилятора. Следовательно это или недоработка спецификации языка, или авторы языка намекают, что за иерархией включения нужно следить вручную.
То есть я про целесообразность отсутствия в коде как макросов, так и директив, как минимум применительно к стражу включения.
Вопрос не в том какой страж включения нужен, даже как автоматическая опция компилятора для всех файлов. Вопрос в том чтобы не использовпть его вообще и управлять вручную иерархией включения так чтобы компилятор не ругался.
уже пришли модули С++
где подход другой
ВР>какая цель преследуется в поиске этой истины ?
Для себя лично. Если я захочу написать что-то, то не буду скован ограничениями.
Это как раньше продвигали венгерскую нотацию. Везде её пихали в проекты и даже книги завязанные на конкретные библиотеки. А сейчас эта тема себя изжила.
Просто может кто-то додумался писать код без стража включения и столкнулся со страшной жестью. У меня такого опыта нет потому и создал тему.
Но я конечно и сам попробую постирать стражи включения раз уж мне пришла такая идея.
V>Вопрос не в том какой страж включения нужен, даже как автоматическая опция компилятора для всех файлов. Вопрос в том чтобы не использовпть его вообще и управлять вручную иерархией включения так чтобы компилятор не ругался.
Когда один простой проект, разницы особой нет. Проблемы начнутся с ростом кодовой базы, разделением её на независимые части. Скажем, есть два разных класса, каждый в своей паре файлов h/cpp, и каждый из них использует структуры или функции, определённые в некоем отдельном заголовочном файле, причём этот дополнительный хэдер написан так, что без стражей он не может быть включён в один cpp дважды. А теперь нам надо написать cpp-файл, где используются оба эти класса. Теперь придётся в одном из классов удалять инклудник. И по цепочке это потребует ручного добавления того же инклюда во всех cpp-файлах, где используется только один этот класс — но только при условии, что тот же инклуд не прилетает с каким-нибудь третьим классом или со стопятидесятой зависимостью какого-то совсем другого инклуда. А потом где-нибудь что-нибудь меняется, и вся иерархия включений летит в тартарары. Попробуй разберись в тысячефайловом проекте, где какие инклуды должны были быть добавлены, а где удалены.
Гораздо удобнее, когда каждый класс является самостоятельной цельной единицей, которую можно просто включить куда угодно, зная, что он сам уже подключает все необходимые ему зависимости, и что повторные включения тех же зависимостей, прилетающие от разных инклудов, не будут друг с другом драться.
CF>Теперь придётся в одном из классов удалять инклудник.
А ещё так же важен порядок включений, а не только иерархия по файлам.
CF>Гораздо удобнее, когда каждый класс является самостоятельной цельной единицей, которую можно просто включить куда угодно
Удобнее может и да. Но ещё удобнее, если бы стандарт языка заставлял каждый файл быть со стражем включения без особой записи. А если хочешь вставить файл несколько раз, тогда пишешь это тоже какой-нибудь конструкцией.
Но вот вопрос в каких случаях нужно вставить один файл в другой несколько раз? Я ни разу не встречался с таким явлением. Может кто опытней знает зачем? И возник вопрос, что имели в виду создатели языка, то есть Страуструп и прочий комитет? Что они подразумевали проектируя препроцессор?
Тот же Страуструп любит рассуждать про логическую и физическую структуру программы. А в итоге получается, что страж включения это костыль, который избавляет нас от ручного управления физической структурой программы и позволяет прописывать сколько угодно включений.
Если это просчёт языка, то ладно. Но может были какие-то особые соображения, чтобы люди думали где ставить #include. А потом другие люди придумали костыли для удобства.
А то вот многие возмущаются, что в C++ для управления памятью надо обладать какими-то особыми знаниями. Я так же почитал книгу "C++ библиотека программиста. Элджер Джефф". Да действительно практических вопросов там больше, чем в книгах по синтаксису языка. Но и результат управления памятью сказывается на производительности.
И может быть здесь так же. Авторы подразумевали, что надо управлять физической структурой программы в ручном режиме, но ленивые программисты решили иначе начав штамповать костыли. Или всё же нет.
К сожалению я этой темой раньше не интересовался и соответственно не помню зачем так сделано даже если читал. А я может быть даже и не читал нигде об этом.
V>Но вот вопрос в каких случаях нужно вставить один файл в другой несколько раз? Я ни разу не встречался с таким явлением. Может кто опытней знает зачем?
pshpack8.h
V>И возник вопрос, что имели в виду создатели языка, то есть Страуструп и прочий комитет? Что они подразумевали проектируя препроцессор?
Конечно же Страуструп и прочий комитет не проектировали препроцессор. Он достался по наследству от сишечки
V>Но вот вопрос в каких случаях нужно вставить один файл в другой несколько раз? Я ни разу не встречался с таким явлением. Может кто опытней знает зачем?
Препроцессорная магия. На голом Си такое делают. На плюсах она нужна в гораздо меньшей степени, но если общая для си и плюсов кодовая база, или если надо чего-то особенного...
V> И возник вопрос, что имели в виду создатели языка, то есть Страуструп и прочий комитет? Что они подразумевали проектируя препроцессор?
Препроцессор проектировал не Страуструп, а Керниган и Ричи.
А С++ его унаследовал. В том числе, для частичной совместимости с Си.
V>Тот же Страуструп любит рассуждать про логическую и физическую структуру программы. А в итоге получается, что страж включения это костыль, который избавляет нас от ручного управления физической структурой программы и позволяет прописывать сколько угодно включений.
К инклудам уже так все привыкли, что нормальные модули сделать не могут.
Как и нормальные экспорты шаблонов.
А тут ещё и бесплатная препроцессорная магия.
CF>Когда один простой проект, разницы особой нет. Проблемы начнутся с ростом кодовой базы, разделением её на независимые части. Скажем, есть два разных класса, каждый в своей паре файлов h/cpp, и каждый из них использует структуры или функции, определённые в некоем отдельном заголовочном файле, причём этот дополнительный хэдер написан так, что без стражей он не может быть включён в один cpp дважды. А теперь нам надо написать cpp-файл, где используются оба эти класса. Теперь придётся в одном из классов удалять инклудник. И по цепочке это потребует ручного добавления того же инклюда во всех cpp-файлах, где используется только один этот класс — но только при условии, что тот же инклуд не прилетает с каким-нибудь третьим классом или со стопятидесятой зависимостью какого-то совсем другого инклуда. А потом где-нибудь что-нибудь меняется, и вся иерархия включений летит в тартарары. Попробуй разберись в тысячефайловом проекте, где какие инклуды должны были быть добавлены, а где удалены.
Это составит проблему, когда библиотеки делали недалёкие люди и использовали в качестве стража включения только имя файла. Я всегда использовал полный путь относительно корня -I, и дополнительно добавлял в страж GUID.
Когда pragma once стала боль менее стандартной, я как-то отвык от гардов. Иногда использую их подобие, когда мне в некоторых местах хочется проверить, не был ли включен какой-либо файл, и определить в зависимости от этого какие-то доп фичи
V>Для чего вообще включать два или более раз одно и тоже, что явно вызовет ошибку компилятора. Следовательно это или недоработка спецификации языка, или авторы языка намекают, что за иерархией включения нужно следить вручную.
Два или больше раз включать одно и тоже можно замечательно, и это не вызовет ошибку компиляции, если ты понимаешь, что делаешь
M>Два или больше раз включать одно и тоже можно замечательно, и это не вызовет ошибку компиляции, если ты понимаешь, что делаешь
Может и не вызвать ошибки компиляции, но зачем? Вот напишешь несколько раз объявления без определений и оно прокатит. Но практического смысла я не вижу. Напиши код который имеет смысл, я хоть посмотрю зачем это кому-то надо было.
M>>Два или больше раз включать одно и тоже можно замечательно, и это не вызовет ошибку компиляции, если ты понимаешь, что делаешь
V>Может и не вызвать ошибки компиляции, но зачем? Вот напишешь несколько раз объявления без определений и оно прокатит. Но практического смысла я не вижу. Напиши код который имеет смысл, я хоть посмотрю зачем это кому-то надо было.
Например, у меня была ситуация, когда шаблонный using не работал как надо (да, не самый новый компилятор), и втащить корректно сущность в другой namespace не получалось. Но каждый раз использовать сущность с указанием её namespace не хотелось. Тогда я поместил эту сущность в файл без namespace, и стал инклюдить куда надо. Да, костыль, да, дублирование кода, да и пофик, зато делает что мне нужно.
M>>>Два или больше раз включать одно и тоже можно замечательно, и это не вызовет ошибку компиляции, если ты понимаешь, что делаешь
V>>Может и не вызвать ошибки компиляции, но зачем? Вот напишешь несколько раз объявления без определений и оно прокатит. Но практического смысла я не вижу. Напиши код который имеет смысл, я хоть посмотрю зачем это кому-то надо было.
M>Например, у меня была ситуация, когда шаблонный using не работал как надо (да, не самый новый компилятор), и втащить корректно сущность в другой namespace не получалось. Но каждый раз использовать сущность с указанием её namespace не хотелось. Тогда я поместил эту сущность в файл без namespace, и стал инклюдить куда надо. Да, костыль, да, дублирование кода, да и пофик, зато делает что мне нужно.
Тут можно продолжить в строну namespace: включаемый код может содержать макросы (такие как BEGIN/END/ASSERT),
которыми можно менять поведение включаемого кода в зависимости от сборки или/и внешнего заголовка,
и получить пространства имён типа debugCall/safeCall/fastCall, которыми можно уже пользоваться осмысленно.
Тут и стражи, и повторное использование, и циклические включения, чего с pragma once не получится добиться.
V>А я подумал о том, что зачем это не сделали по умолчанию опцией компилятора, то есть автоматическим стражем включения для каждого файла.
Ты не видел, как делается метапрограммирование на С? Не видел, как один хедер два раза инклюдят, но с разными дефайнами, чтоб одни и те же алгоритмы для разных типов получить?
V>Читал я тут очередную книгу по C++ и мне внезапно подумалось, а нужны ли стражи включения.
Что-то ты как-то немного отстал от жизни
V>>Читал я тут очередную книгу по C++ и мне внезапно подумалось, а нужны ли стражи включения.
M>Что-то ты как-то немного отстал от жизни
И ещё как. Хотя у меня есть как старые книги так и новейшие, но всегда можно накачать больше.
Лаптев тут советует.
какая лучшая книга по c++? посоветуйте
И читая "Программирование на C++. Хенкеманс Дирк, Ли Марк" мне подумалась эта тема про стражи включения. Хотя я тут несколько разных книг открыл почитать, так что сработали совокупные мысли. Там скорее важно в какую категорию попадёт книга, чем её год. Например, книга по синтаксису и стандартной библиотеке шаблонов не учит алгоритмам или продвинутому владению языком.
M>>Что-то ты как-то немного отстал от жизни
V>И ещё как. Хотя у меня есть как старые книги так и новейшие, но всегда можно накачать больше.
V>Лаптев тут советует.
V>какая лучшая книга по c++? посоветуйте
Лаптеву я бы не очень доверял, он иногда такого насоветует...
Но в целом в списке много полезных книг, которые я читал, и могу сказать, что полезны. Но вот например: "Шамис В.А. Borland C++ Builder 6" — вот зачем это нужно?
Ну и в целом, русских авторов нет смысла читать по этой теме, они глубоко вторичны, из тех наших, кто хорошо в теме, вроде никто книжки не пишет
V>И читая "Программирование на C++. Хенкеманс Дирк, Ли Марк" мне подумалась эта тема про стражи включения. Хотя я тут несколько разных книг открыл почитать, так что сработали совокупные мысли. Там скорее важно в какую категорию попадёт книга, чем её год. Например, книга по синтаксису и стандартной библиотеке шаблонов не учит алгоритмам или продвинутому владению языком.
Никогда не слышал про этих чуваков, не уверен, стоит ли их читать.
В лаптевском списке есть, оттуда наверное взял? Год 2002ой, не уверен, что там что-то полезное для сегодняшнего дня есть.
Вообще, раз такие вопросы возникают, и ты реально не в теме, откуда что взялось, прежде всего прочитай книжки Страуструпа. Их не так много, и они довольно тонкие, кроме одной. Толстую можно на потом оставить, после тонких. Можно начать с "Дизайн и эволюция C++". Когда прочитаешь все тонкие книжки Страуструпа, читай толстую. И только потом уже начинай читать остальных. Тут ты уже будешь сам понимать, тебе трешак втюхали, или что-то годное
M>Вообще, раз такие вопросы возникают, и ты реально не в теме, откуда что взялось, прежде всего прочитай книжки Страуструпа.
У меня по Страуструпу есть даже печатная книга "Язык программирования C++. 3-е издание. 1999 год", как раз купленная где-то тогда мною лично для себя.
Сейчас натравил Notepad++ на текстовые слои электронных книг.
Страж включения упоминается.
В первой как страж включения (include guard), во второй как защитой включения (include guard).
Отрывок из первой книги.
И что показательно, пример подан с точки зрения виндузятников.
M>Можно начать с "Дизайн и эволюция C++". Когда прочитаешь все тонкие книжки Страуструпа, читай толстую. И только потом уже начинай читать остальных. Тут ты уже будешь сам понимать, тебе трешак втюхали, или что-то годное
"Язык программирования C++" Страуструпа это для меня база, но программировать с этой книги не научишься, только синтаксис и возможно введение в STL, именно введение, а не нормальное использование. Я не помню упоминаний стража включения, а видишь, поиск тоже не находит.
Но судя потому, что есть лишь краткое упоминание в "Программирование принципы и практика использования C++" с точки зрения виндузятников, может быть мне действительно "втюхали трешак" виндузятники из майкрософт. Вот у них я часто видел эту конструкцию и книги по всяким DirectX и прочему.
V>У меня по Страуструпу есть даже печатная книга "Язык программирования C++. 3-е издание. 1999 год", как раз купленная где-то тогда мною лично для себя.
Купи новую версию. Как минимум, было ещё Special Edition, в которое долили воды, и ещё почти современное переиздание, 4ое. Но это толстая книга. Прочти сначала тонкие, потом толстую, а потом Майрсов с Александресками дозированно читай, только дозированно, а то будет перетоксикоз шаблонами. Но лучше это на потом, а пока почитай книжки с участием Джосаттиса, толстячки, но читаются легко, у меня две, одна целиком его, другая в соавторстве, может ещё какие-то есть.
Это уже всё довольно старое по нонешним меркам, но подтянутся поможет
курса Констатина Владимирова про андвенсед С++ на ютубе
абсолютно достаточно что бы подтянуть основные часто используемые знания по С++20 включительно
ВР>ну и зачем все эти сборки макулатуры, вы под деда лаптеева косите что ли?
ВР>курса Констатина Владимирова про андвенсед С++ на ютубе
ВР>абсолютно достаточно что бы подтянуть основные часто используемые знания по С++20 включительно
У нас ютуба нет, ты забыл, что ли
V>2. Код со стражами включений менее красивый и более отвлекающий, особенно когда используется конструкция из макросов.
Зачем во включаемых файлах красота? Вы их выставляете в музеях современного искусства?
V>А кто там говорил, что макросы в принципе не нужно использовать, не Страуструп ли Бьерн, создатель языка.
Такого он не говорил. Он говорил, что не нужно использовать макросы с параметрами для того же самого, для чего введены шаблоны. И даже в таком случае допускал, что иногда макрос таки может быть удобнее.
V>по большому счёту их можно и не использовать.
Если файлы небольшие и не конфликтуют друг с другом, то можно и не использовать — сложить все нужные #include еще в один, и включать только его.
А так-то, посмотрите хоть на виндовый SDK/WDK. Это где-то к десятым версиям они более-менее привели иерархию в порядок, а раньше было проще вздернуться, чем подобрать правильный порядок и варианты в нестандартных случаях (например, чтоб использовать некоторые ядерные определения в пользовательском коде, или наоборот).
V>Читал я тут очередную книгу по C++ и мне внезапно подумалось, а нужны ли [url=https://ru.wikipedia.org/wiki/Include_guard]стражи включения[/q
Я в своем хобби-проекте на С не использую, все и без них отлично работает.
Q>Прикольное название: стражи. А есть там рыцари, пажи.. или бояре какие нибудь?
Нету
Было бы надо — появились бы
V>Читал я тут очередную книгу по C++ и мне внезапно подумалось, а нужны ли стражи включения.
Допустим, у тебя есть заголовочник A, который определяет какие-то базовые понятия, и заголовочники B и C, которые этими понятиями пользуются.
Как их использовать? Каждый раз требовать, чтобы программист включал их все, да еще и в правильном порядке?
При наличии механизма, который делает безопасным повторное включение одного и того же заголовочника, зависимые заголовочники B и C могут сами навключать себе то, чти и нужно, не вызывая хаоса в программе.
Проблема ненужных #include понятна. По-хорошему, компилятор мог бы анализировать код и выдавать предупреждения по поводу лишних #include. Но к сожалению, он этого не делает.
Есть и еще одна проблема. Явно включенные заголовочники могут притащить по своим зависимостям какие-то другие, и программа может пользоваться символами из этих неявных включений, сама явно их не включая. Такая конструкция имеет тенденцию иногда ломаться. Тут тоже мог бы помочь компилятор, но увы, он этого не делает.
Pzz>Проблема ненужных #include понятна. По-хорошему, компилятор мог бы анализировать код и выдавать предупреждения по поводу лишних #include. Но к сожалению, он этого не делает.
Clangd делает.
Pzz>Есть и еще одна проблема. Явно включенные заголовочники могут притащить по своим зависимостям какие-то другие, и программа может пользоваться символами из этих неявных включений, сама явно их не включая. Такая конструкция имеет тенденцию иногда ломаться. Тут тоже мог бы помочь компилятор, но увы, он этого не делает.
Пример?
Pzz>>Есть и еще одна проблема. Явно включенные заголовочники могут притащить по своим зависимостям какие-то другие, и программа может пользоваться символами из этих неявных включений, сама явно их не включая. Такая конструкция имеет тенденцию иногда ломаться. Тут тоже мог бы помочь компилятор, но увы, он этого не делает.
S>Пример?
#include <string.h> приносит size_t (из stddef.h). Потом мы string.h> убираем, а stddef.h забываем явно включить, и получаем ататашечку.
Это в libc. В ней все еще более-менее аккуратно сделано. В сторонних библиотеках возможны и более заковыристые варианты, но пример навскидку не приведу.
Pzz>#include <string.h> приносит size_t (из stddef.h). Потом мы string.h> убираем, а stddef.h забываем явно включить, и получаем ататашечку.
1. Clangd умеет предупреждать когда необходимые объявления не включены явно.
2. Clangd умеет предупреждать когда необходимые объявления не включены.
2. Ошибка во время компиляции это не проблема ни разу.
S>Здравствуйте, Pzz, Вы писали:
Pzz>>#include <string.h> приносит size_t (из stddef.h). Потом мы string.h> убираем, а stddef.h забываем явно включить, и получаем ататашечку.
S>1. Clangd умеет предупреждать когда необходимые объявления не включены явно.
S>2. Clangd умеет предупреждать когда необходимые объявления не включены.
Я не знал. Я как-то больше привык к gcc
S>2. Ошибка во время компиляции это не проблема ни разу.
Она становится проблемой, когда у тебя несколько разных дистрибутивов линуха под присмотром, и ошибка вылезнает не на том/тех, на которых ты обычно работаешь. К сожалению, разные опции сборки библиотек и компиляторов иногда "радуют" такими сюрпризами.
Pzz>Я не знал. Я как-то больше привык к gcc
Так я тоже в основном gcc использую, но Clangd это не компилятор, а "языковой сервер" (построенный на базе компилятора Clang). Его редакторы и среды разработки используют.
Pzz>Она становится проблемой, когда у тебя несколько разных дистрибутивов линуха под присмотром, и ошибка вылезнает не на том/тех, на которых ты обычно работаешь. К сожалению, разные опции сборки библиотек и компиляторов иногда "радуют" такими сюрпризами.
Я с этим не спорю, я больше про то, что такие ошибки это не большая проблема в принципе, да надо исправить и пересобрать, но это не UB или SEGFAULT во время выполнения.