Pull to refresh

Comments 27

в заголовочных файлах можно использовать #pragma once. Но данный способ работает не на всех компиляторах и, поэтому, не всегда применим.

Друзья, а кто в последнее время сталкивался с такой «несовместимостью»? А то ощущение, что давно несуществующую проблему переписывают из статьи в статью :)
Проблема только в том, что это не описывается стандартом.

А по существу, и GCC и MSVC (и даже VisualDSP++) поддерживает данное расширение.
Если верить en.wikipedia.org/wiki/Pragma_once, то и clang и icc поддерживают. Поэтому я и спрашиваю, насколько include guards необходимы :)
За pragma надо заставлять учиться стандарт наизусть и пересказывать с выражением.
Ну или более гуманно, убивать.
Что, даже за #pragma warning?
А вы готовы указать мне на раздел стандарта, где что-то говорится про pragma warning?
Я ошибся. Перепутал с директивой #warning, который тоже не стандартна.
Ну Вы же пишете с отклонениями от норм русского языка, но мы Вас понимаем и не убиваем ;) Может и здесь не обязательно быть дотошными занудами, коль скоро все компиляторы нас понимают?
Если бы мои комментария должны были быть понятны всем и если бы у русского языка были строгие семантические нормы — да, можно было бы и убивать.

Зачем вообще существуют стандарты? И почему ситуация с IE6 огорчала, а схожая ситуация с pragma — хорошо?
Вот 3 С++ компилятора, не поддерживающие pragma once

Sun CC
IBM xlC
HP-UX aCC
UFO just landed and posted this here
Вот Вы будете плагин к какому-нибудь MQ писать на AIX, я посмотрю, чем Вы будете пользоваться :-)
#pragma once выглядит как эпичный костыль. Лучше бы в стандарт языка ввели import!
#pragma once выглядит немного лучше, чем #ifndef #define #endif. Но в целом, согласен.
Только лучше не просто import, а полноценные модули в C++.
Кстати, а есть какое-то каноническое определение термина «модуль»?
Интуитивно я понимаю чем модуль отличается от простого включения текста файла в другой файл, но, возможно есть другие обязательные черты, вроде обязательного наличия конструкторов модулей?
Не скажу за всех, но лично в моем понятии модуль обеспечивает:

1) Явное разделение интерфейса и реализации.
2) Возможность импорта имен из другого модуля и создания псевдонимов импортируемых элементов.
3) Обособленность модулей. Т.е. изменения в одном модуле, не затрагивающие его интерфейса, не влияют на определения в других модулях.
4) Так называемые конструкторы/деструкторы модулей и, как следствие, определенный порядок их вызова.

По пункту 1.
в С++ имеется возможность разбивать исходные коды на заголовочные файлы (*.h, *.hpp) и файлы реализации (*.cpp). Также в этом помогают пространства имен и идиома PImpl, за использование которой приходится платить динамическим созданием, косвенным доступом к членам класса impl и дополнительным кодом.

По пункту 2.
В C++ нельзя задать псевдонимы для функций и для шаблонных классов (в С++11, наконец-то, появились alias-ы для шаблонных классов). А введение элементов из других пространств имен в заголовочных файлах, в общем случае, не приветствуется.

Пункт 3.
Если файлы реализации можно обрабатывать независимо друг от друга, то заголовочные файлы нельзя. Поэтому, если файл реализации зависит от нескольких «модулей», то заголовочные файлы каждого модуля компилируются вместе. Как следствие, возможны сюрпризы с порядком включения, #define-ами, специализациями шаблонов и другое.

Пункт 4.
По стандарту C++ порядок инициализации глобальных объектов не определен. Есть обходные пути, но не на все случаи.

Дополнительно хочется отметить, что введение модулей, по идее, должно значительно уменьшить время компиляции. Сейчас в C++ одно и то же компилируется по нескольку раз в каждой единице трансляции (прекомпилируемые заголовки не всегда помогают).
"В предыдущей части был приведен способ..."
Было бы хорошо добавить ссылку на предыдущую часть.
Спасибо, поправил.
Думаю абзац про «pragma once» лучше дополнить тем, что было высказано в комментариях.
думаю, что вопрос про «pragma once» вообще зря затронут в этой статье, всё обсуждение свелось к нему)
От себя добавлю, что порядок инклюдов хорошо бы вести такой:
а) публичные инклюды SomeLib
б) внутренние инклюды SomeLib
в) инклюды 3rd-party либ (boost, Qt, libpng etc)
г) инклюды STL

Это позволяет жестче следить за самодостаточностью файлов (п 4)
Общее правило: Чем более «низкоуровневый» файл включается, тем ниже он в списке.
В данном порядке Ваш вариант не подходит, поскольку открытый (публичный) интерфейс очень часто зависит от сторонних библиотек (STL, boost и др.).

Но если его обратить, то вполне:
а) #include STL;
б) #include 3rd-party библиотек (boost, Qt, libpng и т.д.);
в) внутренние #include библиотеки SomeLib;
г) публичные #include библиотеки SomeLib.

Единственное исключение — это то, что варианты в) и г) могут перемешиваться. Бывает так, что детали реализации завязаны на открытый интерфейс. (еще раз повторюсь мы рассматриваем библиотеки в варианте заголовочных файлов).

Если реализацию методов классов выносить в отдельные файлы, то их, конечно, нужно подключать самыми последними.
А в чем проблема в публичный интерфейс включить нужные инклюды? Интерфейс на то и интерфейс, чтобы определять всё необходимое для работы.
Пример (хедергарды опущены):
a.h: // интерфейс
#include // в интерфейсе будет вектор. Инклюдим.
class A
{
public:
virtual ~A(){};
virtual std::vector getVector() = 0;
}

b.h: // реализация
#include "a.h"
#include // используем вектор? Опять инклюдим.

class B : public A
{
public:
virtual ~B(){};
virtual std::vector getVector()
{
// реализация
return m_Data;
};
private:
std::vector m_Data;
}

c.h:
// нет инклюда вектора! Забыли включить, например. (1)
struct C
{
std::vector arr; // (2) см ниже.
}

use1.cpp: // Ваш вариант.
#include // СТЛ в начале
#include "c.h" // (2) не ругается
#include "a.h"

use2.cpp: // тут самое интересное
#include "c.h" // (3) ругается
#include "a.h"
#include // СТЛ в конце

use3.cpp
#include "a.h"
#include "c.h" // (4) не ругается ни в каком случае.
}


Может, я чего-то не понимаю в этой жизни, но при моем варианте я при ошибке в use2.cpp в месте (3) полезу в (1) и вставлю недостающий инклюд. В Вашем случае (use1.cpp) ошибки сборки не будет и файл c.h, вероятно, так и останется с недочетом.
Более того, если поменять включение "a.h" и "c.h" местами (как это сделано в use3.cpp) - то всё будет собираться. И ломать голову, почему в (4) всё собирается, а в (2) - нет - ну хотя бы лишняя потеря времени.

Самодостаточность файлов это включает в себя независимый порядок их включения, разве не так?
Блин. Надеялся я на парсер, а он всё равно < и > съел. Надеюсь, суть понятна.
>А в чем проблема в публичный интерфейс включить нужные инклюды?
>Интерфейс на то и интерфейс, чтобы определять всё необходимое для работы.
Так они и так там включаются…

Если рассматривать порядок включения заголовочных файлов в файлах реализации (cpp), то, действительно, Ваш подход может показать несамодостаточность заголовочных файлов. Но не всегда, и, особенно, в тех случаях, когда включений немало. Некоторые примеры Вы привели сами.

Самый верный способ проверить самостоятелен ли заголовочный файл — это подключить его одного в отдельном файле cpp, и, конечно, не подключать в него больше ничего.

Встречный вопрос, как Ваш подход применим при реализации библиотеки разработчиком (при поставке ее только в виде заголовочных файлов)?

>Самодостаточность файлов это включает в себя независимый порядок их включения, разве не так?
Конечно, так. Статья, как раз, и нацелена показать, что сделать самодостаточными каждый заголовочный файл с учетом всех зависимостей — «нудная» задача и ее можно решить по-другому.

(Кстати, прежде чем нажать кнопку «Написать», можно выбрать кнопку «Предпросмотр» и увидеть как будет выглядеть комментарий)
>Конечно, так. Статья, как раз, и нацелена показать, что сделать самодостаточными каждый заголовочный файл с учетом всех зависимостей — «нудная» задача и ее можно решить по-другому.
Буквально сегодня, разбирая старый проект, в котором используется метод «единого включения», наткнулся на вышеописанную мной проблему. В одном из заголовочных файлов нужна была банальщина, версия библиотеки. Чтобы не тянуть все-все хедеры кто-то когда-то сэкономил и включил только один файл с нужным функционалом. Всё работало до тех пор, когда этот файл собирался после другого, в котором «всё включено». Так вот, убирал я «мертвый» код из проекта, и получилось так, что этот «неправильно включенный» хедер начал собираться раньше.
Получается что? Изменился пользовательский код (который раньше работал корректно), а ошибку компилятор выдает во внутренних заголовочных файлах либы, которую никто и не трогал. Неаккуратненько и фрустрирует немного.

>А если теперь задаться вопросом: действительно ли, так необходимо реализовывать для пользователя библиотеки возможность подключения отдельных ее элементов?
Хорошо, когда SomeLib по объему не больше boost::shared_ptr. Тогда действительно, можно на вопрос ответить однозначно «нет»: написал один раз и забыл. Включай да пользуй.
Но в чем-то более крупном лучше я потрачу на 5 минут больше времени (на свежую голову), проверив все зависимости, чем буду через год голову ломать «почему меняя инклюды местами всё перестаёт собираться»
Как там писали? Чем позже найдена ошибка, тем дороже её устранение.

>Встречный вопрос, как Ваш подход применим при реализации библиотеки разработчиком (при поставке ее только в виде заголовочных файлов)?
На личном примере и советую, причем как со стороны разработчика библиотеки, так и со стороны её использования. И правило «напиши код так, чтобы неправильно использовать библиотеку было трудно» не раз себя хорошо зарекомендовал.

Да и вообще. Как-то простой совет с порядком инклюдов перерастает в сочинение «Один мой день», не нравится мне это.
Sign up to leave a comment.

Articles