Явные прямые #include против недоговорных переходных #include

Скажем, у нас есть этот заголовочный файл:

MyClass.hpp

#pragma once
#include <vector>

class MyClass
{
public:
    MyClass(double);

    /* ... */

private:
    std::vector<double> internal_values;
};

Теперь, когда мы используем #include "MyClass.hpp" в каком-либо другом файле #include "MyClass.hpp" или cpp, мы также эффективно #include <vector>, несмотря на тот факт, что нам это не нужно. Причина, по которой я говорю, что в этом нет необходимости, заключается в том, что std::vector используется только внутренне в MyClass, но вообще не требуется, чтобы фактически взаимодействовать с этим классом.

В результате я мог написать

Версия 1: SomeOtherHeader.hpp

#pragma once
#include "MyClass.hpp"

void func(const MyClass&, const std::vector<double>&);

тогда как я наверное должен написать

Версия 2: SomeOtherHeader.hpp

#pragma once
#include "MyClass.hpp"
#include <vector>

void func(const MyClass&, const std::vector<double>&);

предотвратить зависимость от внутренней работы MyClass. Или я должен?

Я очевидно понимаю, что MyClass нуждается в <vector> для работы. Так что это может быть больше философским вопросом. Но разве не было бы хорошо иметь возможность решить, какие заголовки будут отображаться при импорте (то есть ограничивать то, что загружается в пространство имен)? Так что каждый заголовок должен #include то, что ему нужно, не уходя, неявно включая что-то, что нужно другому заголовку в цепочке?

Может быть, люди также смогут пролить свет на будущие модули С++ 20, которые, я считаю, касаются некоторых аспектов этой проблемы.

Ответ 1

предотвратить зависимость от внутренней работы MyClass. Или я должен?

Да, вы должны и в значительной степени по этой причине. Если вы не хотите указать, что MyClass.hpp гарантированно включает <vector>, вы не можете полагаться на одно, включая другое. И нет никаких веских причин быть вынужденным предоставить такую гарантию. Если такой гарантии нет, то вы полагаетесь на детали реализации MyClass.hpp, которые могут измениться в будущем, что нарушит ваш код.

Я очевидно понимаю, что MyClass нужен вектор для работы.

Является ли? Разве он не может использовать вместо этого, например, boost::container::small_vector?

В этом примере MyClass нужен std :: vector

Но как насчет потребностей MyClass в будущем? Программы развиваются, и то, что нужно классу сегодня, не всегда то, что нужно классу завтра.

Но не было бы хорошо иметь возможность решить, какие заголовки будут отображаться при импорте

Предотвращение транзитивного включения невозможно.

Модули, представленные в С++ 20, являются функцией, которая может использоваться вместо pp-включения и предназначены для решения этой проблемы.

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

Ответ 2

Вы должны использовать явный #include чтобы иметь неразрушающий рабочий процесс. Допустим, MyClass используется в 50 различных исходных файлах. Они не включают vector. Внезапно, вы должны изменить std::vector в MyClass.h для какого-то другого контейнера. Тогда все 50 исходных файлов должны будут включать vector или вам нужно будет оставить его в MyClass.h. Это было бы избыточно и могло бы излишне увеличить размер приложения, время компиляции и даже время выполнения (инициализация статической переменной).

Ответ 3

Учтите, что код должен быть написан не просто один раз, а с течением времени.

Предположим, вы написали код, и теперь моя задача состоит в его рефакторинге. По какой-то причине я хочу заменить MyClass на YourClass и, скажем, у них одинаковый интерфейс. Я бы просто заменил любой YourClass MyClass на YourClass чтобы прийти к следующему:

/* Version 1: SomeOtherHeader.hpp */

#pragma once
#include "YourClass.hpp"

void func(const YourClass& a, const std::vector<double>& b);

Я все сделал правильно, но все равно код не скомпилируется (потому что YourClass не включает std::vector). В этом конкретном примере я получу четкое сообщение об ошибке, и исправление будет очевидным. Однако все может стать довольно запутанным, если такие зависимости охватывают несколько заголовков, если таких зависимостей много и если SomeOtherHeader.hpp содержит больше, чем просто одно объявление.

Есть еще вещи, которые могут пойти не так. Например, автор MyClass мог решить, что он действительно может отказаться от включения в пользу предварительной декларации. Также тогда SomeOtherHeader сломается. Это сводится к следующему: если вы не SomeOtherHeader vector в SomeOtherHeader то существует скрытая зависимость, что плохо.

Основное правило для предотвращения таких проблем: Включите то, что вы используете.

Ответ 4

Если ваш MyClass имеет член типа std::vector<double> тогда заголовок, который определяет MyClass должен #include <vector>. В противном случае пользователи MyClass могут компилировать только один раз, если они #include <vector> перед включением определения MyClass.

Хотя член является private, он все еще является частью класса, поэтому компилятору необходимо увидеть полное определение типа. В противном случае он не может делать такие вещи, как вычисление sizeof(MyClass) или создание каких-либо объектов MyClass.

Если вы хотите разорвать зависимость между вашим заголовком и <vector> есть методы. Например, идиома pimpl ("указатель на реализацию").

class MyClass 
{
public:
    MyClass(double first_value);

    /* ... */

private:
    void *pimpl;
};

и в исходном файле, который определяет членов класса;

#include <vector>
#include "MyClass.hpp"

MyClass::MyClass(double first_value) : pimpl(new std::vector<double>())
{

}

(а также, предположительно, сделать что-то с first_value, но я это опустил).

Компромисс состоит в том, что каждая функция-член, которая должна использовать вектор, должна получить его из pimpl. Например, если вы хотите получить ссылку на выделенный вектор

void MyClass::some_member_function()
{
    std::vector<double> &internal_data = *static_cast<std::vector<double> *>(pimpl);

}

Деструктор MyClass также должен освободить динамически размещенный вектор.

Это также ограничивает некоторые варианты определения класса. Например, MyClass не может иметь функцию-член, которая возвращает значение std::vector<double> (если вы не #include <vector>)

Вам нужно решить, стоят ли такие методы, как идиома pimpl, чтобы ваш класс работал. Лично, если нет каких-либо ДРУГИХ веских причин отделить реализацию класса от класса с помощью идиомы pimpl, я бы просто согласился с необходимостью #include <vector> в вашем заголовочном файле.

Ответ 5

Да, используемый файл должен явно указывать <vector>, так как это зависимость, которая ему нужна.

Однако я бы не стал беспокоиться. Если кто-то реорганизует MyClass.hpp для удаления включения <vector>, компилятор будет указывать им на каждый файл, в котором отсутствует явное включение <vector>, полагаясь на неявное включение. Обычно исправлять ошибки такого типа не составляет труда, и после повторной компиляции кода некоторые из отсутствующих явных включений будут исправлены.

В конце концов, компилятор гораздо эффективнее обнаруживает пропущенные включения, чем любой человек.

Ответ 6

Вы бы раздражали потребителей вашего класса, если бы они не забыли #include <vector>.

Поэтому, поскольку internal_values требует, чтобы std::vector был полным типом, вы должны явно включить <vector> в свой заголовок.

Ответ 7

Как уже говорили другие, безопаснее напрямую включать файлы, которые вы используете, с точки зрения защиты от будущих изменений в файле, на который вы полагаетесь при пересылке.

Также обычно считается чище иметь там свои зависимости сразу. Если вы хотите проверить, что это за объект "MyClass", просто прокрутите вверх и попросите свою среду разработки перенести вас в соответствующий заголовок.

Стоит отметить, что безопасно включать один и тот же стандартный заголовок несколько раз, как это предусмотрено стандартной гарантией библиотеки. На практике это означает, что реализация (скажем, clang libc++) начнется с защитой #include. Современные компиляторы настолько знакомы с идиомой включения защиты (особенно в применении их собственных реализаций стандартной библиотеки), что могут даже не загружать файлы. Таким образом, единственное, что вы теряете в обмен на эту безопасность и ясность, - это набирать дополнительные дюжины или около того букв.

Все это было согласовано со всеми остальными, я перечитал это, и я не думаю, что ваш вопрос был на самом деле "Должен ли я сделать это?" столько, сколько "Почему мне даже разрешено не делать этого?" Или "Почему компилятор не изолирует меня от включенных включений?"

Есть одно важное исключение из правила "непосредственно включайте то, что вы используете". Это заголовки, которые, как часть их спецификации, включают дополнительные заголовки. Например, < iostream > (который, конечно, сам является частью стандартной библиотеки) с c++ 11 гарантированно включает в себя <istream> и <ostream>. Кто-то может сказать: "Почему бы просто не переместить содержимое <istream> и <ostream> <iostream> непосредственно в <iostream>?" но есть и ясность и преимущества в скорости компиляции, так как есть возможность разбить их, если нужен только один. (И, без сомнения, для c++ есть и исторические причины) Вы, конечно, можете сделать это и для своих собственных заголовков. (Это скорее объект Objective-C, но они включают в себя те же механики и традиционно используют их для зонтичных заголовков, единственной задачей которых является включение других файлов.)

Есть еще одна фундаментальная причина, по которой включаемые заголовки включаются. То есть, как правило, ваши заголовки не имеют смысла без них. Предположим, что ваш файл MyClass.hpp содержит следующий синоним типа

using NumberPack = std::vector<unsigned int>;

и следующая самоописательная функция

NumberPack getFirstTenNumers();

Теперь предположим, что другой файл включает в себя MyClass.hpp и имеет следующее.

NumberPack counter = getFirstTenNumbers();
for (auto c : counter) {
    std::cout << c << "\n"
}

Здесь происходит то, что вы можете не захотеть записывать в свой код, который вы используете, <vector>. Это деталь реализации, о которой вам не нужно беспокоиться. Насколько вам известно, NumberPack может быть реализован в виде какого-либо другого контейнера, итератора, или типа генератора, или чего-то еще, при условии, что он соответствует его спецификации. Но компилятор должен знать, что это на самом деле: он не может эффективно использовать родительские зависимости, не зная, что такое заголовки дедушки и бабушки. Побочным эффектом этого является то, что вам сойдет с рук их использование.

Или, конечно, третья причина просто "Потому что это не c++". Да, у кого-то мог быть язык, на котором не передавались зависимости второго поколения, или вы должны были явно запросить его. Просто это будет другой язык, и, в частности, он не будет соответствовать старому тексту, включающему стиль c++ или друзей.