Явные прямые #include против недоговорных переходных #include - PullRequest
21 голосов
/ 14 июня 2019

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

MyClass.hpp

#pragma once
#include <vector>

class MyClass
{
public:
    MyClass(double);

    /* ... */

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

Теперь, когда мы используем #include "MyClass.hpp" в каком-либо другом файле 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 того, что нужно 1033 *, без необходимости косвенно включать в цепочку что-то, что еще один заголовок необходим в цепочке?

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

Ответы [ 6 ]

16 голосов
/ 14 июня 2019

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

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

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

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

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

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

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

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

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

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

7 голосов
/ 14 июня 2019

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

3 голосов
/ 14 июня 2019

Если ваш 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> в вашем заголовочном файле.

3 голосов
/ 14 июня 2019

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

Предположим, что вы написали код, и теперь моей задачей будет его рефакторинг.По какой-то причине я хочу заменить MyClass на 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 сломается.Это сводится к следующему: если вы не включите vector в SomeOtherHeader, то есть скрытая зависимость, что плохо.

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

1 голос
/ 14 июня 2019

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

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

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

0 голосов
/ 15 июня 2019

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

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

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

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

Есть одно важное исключение из правила "непосредственно включайте то, что вы используете".Это заголовки, которые, как часть их спецификации , включают дополнительные заголовки.Например, <<a href="https://en.cppreference.com/w/cpp/header/iostream" rel="nofollow noreferrer">iostream> (который, конечно, сам является частью стандартной библиотеки), начиная с c ++ 11, гарантированно включает <istream> и <ostream>.Кто-то может сказать: «Почему бы просто не перенести содержимое <istream> и <ostream> в <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 ++ или друзей.

...