Очистить операторы #include?

Как вы поддерживаете операторы #include в своем проекте C или C ++? Кажется почти неизбежным, что в конечном итоге набор операторов include в файле либо окажется недостаточным (но может работать из-за текущего состояния проекта), либо включает в себя то, что больше не нужно.

Создавали ли вы какие-либо инструменты для выявления или устранения проблем? Какие-либо предложения?

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

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

Однако, прежде чем что-то создать, я подумал, что должен спросить здесь. Это кажется несколько универсальной проблемой.


person criddell    schedule 18.06.2009    source источник
comment
Хороший план, если у вас нет условной компиляции в исходном коде или заголовках - ваши тесты могут не привести к полному покрытию кода.   -  person Mark Ransom    schedule 18.06.2009
comment
Почему бы просто не скомпилировать сам заголовочный файл? Если ваш компилятор поддерживает это, делайте это, ничего не выводя (например, gcc -fsyntax-only).   -  person CB Bailey    schedule 19.06.2009
comment
См. Также: stackoverflow.com/questions/74326/ и stackoverflow.com/questions/614794/   -  person Eclipse    schedule 19.06.2009


Ответы (12)


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

Вы получите тот же эффект, установив следующее правило: первый файл заголовка, который должен включать foo .c или foo .cpp, должен иметь соответствующее имя foo .h. Это гарантирует, что foo .h включает все, что необходимо для компиляции.

Кроме того, в книге Лакоса Крупномасштабный дизайн программного обеспечения C ++ (например) перечислено множество методов для перемещения деталей реализации из заголовка в соответствующий файл CPP. Если вы доведете это до крайности, используя такие методы, как Чеширский кот (который скрывает все детали реализации) и Factory (который скрывает существование подклассов), тогда многие заголовки смогут работать самостоятельно, не включая другие заголовки, и вместо этого обходиться только одним вместо этого передайте объявление непрозрачным типам ... за исключением, возможно, классов шаблонов.

В конце концов, в каждый файл заголовка может потребоваться включить:

  • Отсутствуют файлы заголовков для типов, которые являются членами данных (вместо этого члены данных определены / скрыты в файле CPP с использованием техники «чеширского кота», также известной как «pimpl»)

  • Отсутствуют файлы заголовков для типов, которые являются параметрами для методов или возвращают типы из методов (вместо этого это предопределенные типы, такие как int; или, если они определяемые пользователем типы, то они являются ссылками, и в этом случае объявленный вперед непрозрачный тип достаточно объявления типа class Foo; вместо #include "foo.h" в файле заголовка).

Тогда вам понадобится файл заголовка для:

  • Суперкласс, если это подкласс

  • Возможно, любые шаблонные типы, которые используются в качестве параметров метода и / или возвращаемых типов: очевидно, вы также должны иметь возможность пересылать объявления классов шаблонов, но некоторые реализации компилятора могут иметь с этим проблемы (хотя вы также можете инкапсулировать любые шаблоны например, List<X> как детали реализации пользовательского типа, например ListX).

На практике я мог бы создать "standard.h", который включает все системные файлы (например, заголовки STL, типы, специфичные для O / S и / или любые #defines и т. Д.), Которые используются любыми / всеми файлами заголовков в проекте, и включить его в качестве первого заголовка в каждый файл заголовка приложения (и указать компилятору рассматривать этот "standard.h" как "предварительно скомпилированный файл заголовка").


//contents of foo.h
#ifndef INC_FOO_H //or #pragma once
#define INC_FOO_H

#include "standard.h"
class Foo
{
public: //methods
  ... Foo-specific methods here ...
private: //data
  struct Impl;
  Impl* m_impl;
};
#endif//INC_FOO_H

//contents of foo.cpp
#include "foo.h"
#include "bar.h"
Foo::Foo()
{
  m_impl = new Impl();
}
struct Foo::Impl
{
  Bar m_bar;
  ... etc ...
};
... etc ...
person ChrisW    schedule 18.06.2009
comment
Теперь, когда я думаю об этом, вы абсолютно правы. Круто то, что я делал это годами, даже не задумываясь о том, почему. Мне нравится, когда я по невежеству поступаю правильно. - person criddell; 18.06.2009
comment
Это верно. Но было бы неправильно просто включать файлы заголовков, если можно обойтись без предварительных объявлений. Таким образом, для любых возвращаемых значений, параметров и типов указателей не следует включать заголовки для этих типов, даже если компиляция завершится неудачно без предварительного объявления типов. - person coombez; 18.06.2009
comment
Это разумный совет, который систематизирован Центром космических полетов имени Годдарда НАСА в их стандартах кодирования C и C ++. - person Jonathan Leffler; 18.06.2009
comment
Обратите внимание: при переходе между реализациями вы можете обнаружить, что разные стандартные заголовки включают (или не включают) заголовки, которые на самом деле нужны вашему проекту. Это неприятно, но санкционировано стандартом C ++, поскольку любой из стандартных заголовков может включать любые другие. - person Jonathan Leffler; 18.06.2009
comment
Вы можете заранее объявить классы шаблонов, если вы не можете напрямую объявить классы шаблонов стандартной библиотеки, потому что это неопределенное поведение. - person Rexxar; 19.06.2009
comment
@Rexxar Я этого не знал, но вижу, что ты прав, например. cpptalk.net/forward-declaration-and-template-vt12686.html < / а> - person ChrisW; 19.06.2009
comment
Сначала это звучало хорошо, но я сомневаюсь, что использование standard.h, как описано, является хорошей практикой. Если вы вносите какие-либо изменения в заголовок, ваш компилятор просматривает все заголовки. Также я проверил упомянутый выше стандарт кодирования НАСА. Разрешите мне разместить: 3.3.7 Каждый файл заголовка должен # включать файлы, необходимые для компиляции, а не заставлять пользователей # включать необходимые файлы. #includes должно быть ограничено тем, что нужно заголовку; другие #includes должны быть помещены в исходный файл. Я бы посоветовал поступить так, как указано. Но мне нравится описанный выше способ проверки. - person AudioDroid; 06.01.2011

У меня есть привычка упорядочивать свои включения от высокого уровня абстракции до низкого уровня абстракции. Для этого необходимо, чтобы заголовки были самодостаточными, а скрытые зависимости быстро выявлялись как ошибки компилятора.

Например, у класса «Тетрис» есть файлы Tetris.h и Tetris.cpp. Порядок включения Tetris.cpp будет следующим:

#include "Tetris.h"     // corresponding header first
#include "Block.h"      // ..then application level includes
#include "Utils/Grid.h" // ..then library dependencies
#include <vector>       // ..then stl
#include <windows.h>    // ..then system includes

И теперь я понимаю, что это не совсем ответ на ваш вопрос, поскольку эта система на самом деле не помогает очистить ненужные включения. Ах хорошо..

person StackedCrooked    schedule 18.06.2009

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

person Seth Johnson    schedule 18.06.2009

Обнаружение лишних включений уже обсуждалось в этом вопросе.

Мне не известны какие-либо инструменты, которые помогли бы обнаружить, что недостаточно, но что-то не получается, но здесь могут помочь хорошие соглашения о кодировании. Например, Google C ++ Style Guide требует следующего, с целью уменьшения скрытых зависимостей:

В dir/foo.cc, основной целью которого является реализация или тестирование материала в dir2/foo2.h, упорядочьте свои включения следующим образом:

  1. dir2/foo2.h (предпочтительное местоположение - подробности см. Ниже).
  2. Системные файлы C.
  3. Системные файлы C ++.
  4. Файлы .h других библиотек.
  5. Your project's .h files.
person Josh Kelley    schedule 18.06.2009

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

  1. Специализация шаблона: если у вас есть специализация шаблона для определенного типа, который находится в одном заголовке, а более общий шаблон - в другом, удаление специализации может оставить код в компилируемом состоянии, но с нежелательными результатами.

  2. Разрешение перегрузки: аналогичная проблема - если у вас есть две перегрузки одной функции в разных заголовках, но которые принимают несколько совместимые типы, вы можете удалить версию, которая лучше всего подходит в одном случае, но все же код компилируется. Вероятно, это менее вероятно, чем версия со специализацией шаблона, но возможно.

person Eclipse    schedule 19.06.2009
comment
3. Компиляция кода, при которой отсутствуют объявления для функций, определенных в этом файле, изначально это нормально, но означает, что объявления функций и фактическая функция могут перестать синхронизироваться, и никто этого не заметит. Я столкнулся с этим с помощью техники удаления и перекомпиляции, но обнаружил, что могу использовать sparse (оболочку gcc) для обнаружения этих случаев. - person ideasman42; 29.02.2012

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

Я думаю, что это заблуждение и приведет к "недостаточным, но просто работающим" включенным наборам.

Предположим, что ваш исходный файл использует numeric_limits, но также включает некоторый файл заголовка, который по собственным причинам включает <limits>. Это не означает, что ваш исходный файл не должен включать <limits>. Этот другой файл заголовка, вероятно, не задокументирован для определения всего, что определено в <limits>, просто так случилось. Когда-нибудь он может остановиться: возможно, он использует только одно значение в качестве параметра по умолчанию для какой-то функции, и, возможно, это значение по умолчанию изменится с std::numeric_limits<T>::min() на 0. И теперь ваш исходный файл больше не компилируется, и разработчик этого файла заголовка даже не знал, что ваш файл существует, пока не сломал его сборку.

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

person Steve Jessop    schedule 19.06.2009
comment
Это особенно применимо к системным заголовкам. FWIW Я мог бы сделать standard.h, который включает все системные файлы (например, заголовки STL), используемые любым файлом в проекте, и включить его в качестве первого заголовка в каждый файл заголовка приложения (и указать компилятору обработать этот standard.h как «предварительно скомпилированный заголовочный файл»). - person ChrisW; 19.06.2009
comment
Да, я думаю, что с системными заголовками вы не особо заботитесь о добавлении ненужного. Его можно предварительно скомпилировать, и в любом случае вы их никогда не меняете, чтобы не провоцировать ненужные пересборки. В больших проектах, конечно, действительно плохая идея иметь такой заголовок гибели, который включает в себя все заголовки пользователя. - person Steve Jessop; 19.06.2009

Если вы используете компилятор Visual Studio, вы можете попробовать параметр компилятора / showIncludes, а затем проанализировать то, что он излучает, в stderr. MSDN: «Заставляет компилятор вывести список включаемых файлов. Также отображаются вложенные включаемые файлы (файлы, которые включены из файлов, которые вы включаете)».

person Andrey.Dankevich    schedule 16.02.2011

Ага. У нас есть собственный препроцессор, который дает нам доступ к нашему собственному макроязыку. Он также проверяет, что файлы заголовков включаются только один раз. Создание простого препроцессора, проверяющего несколько включений, должно быть довольно простым.

person ralphtheninja    schedule 18.06.2009
comment
препроцессор не ограничивает вас включением файла заголовка только один раз. Дело в том, что каждый файл заголовка, созданный здравомыслящим человеком, окружен ‹pre› #ifndef MY_HEADER_H #define MY_HEADER_H // кодом заголовка #endif ‹/pre› - person Earlz; 18.06.2009
comment
Что ж, я полагаю, OP на самом деле не спрашивал о нескольких включениях, но возникает вопрос: как вы поддерживаете операторы #include в своем проекте C или C ++? [. . .] Создавали ли вы какие-либо инструменты для выявления или устранения проблем? Какие-либо предложения? Я не знаю, почему это было отклонено - person Carson Myers; 18.06.2009
comment
Один раз я использовал #pragma, но думаю, что это почти то же самое в большинстве современных компиляторов. Я предпочитаю его, потому что он меньше печатает и меньше подвержен ошибкам, чем попытки. - person criddell; 18.06.2009

Что касается инструментов, я использовал Imagix (это было около 6 лет назад) в Windows, чтобы определить включения, которые не нужны, а также включения, которые необходимы, но косвенно включены через другое включение.

person no-op    schedule 18.06.2009

Взгляните на проект cppclean. Хотя они еще не реализовали эту функцию, но это планируется сделать.

С сайта проекта:

CppClean пытается найти проблемы в исходном коде C ++, которые замедляют разработку, особенно в больших базах кода. Он похож на линт; однако CppClean фокусируется на поиске глобальных межмодульных проблем, а не на локальных проблемах, подобных другим инструментам статического анализа.

Цель состоит в том, чтобы найти проблемы, которые замедляют разработку в больших базах кода, которые со временем изменяются, оставляя неиспользуемый код. Этот код может иметь множество форм: от неиспользуемых функций, методов, элементов данных, типов и т. Д. До ненужных директив #include. Ненужные #includes могут вызвать значительные дополнительные компиляции, увеличивая цикл редактирования-компиляции-выполнения.

И особенно о функции #include:

  • (planned) Find unnecessary header files #included
    • No direct reference to anything in the header
    • Заголовок не нужен, если вместо этого были объявлены классы вперед
  • (запланировано) Исходные файлы, которые ссылаются на заголовки не напрямую #included, т. е. файлы, которые полагаются на транзитивный #include из другого заголовка

Здесь вы можете найти зеркало BitBucket.

person Roman Kruglov    schedule 09.04.2012

Если вы пишете код в Eclipse с CDT, вы можете использовать команду «Организовать включает». Просто нажмите Ctrl + Shift + O, и он добавит необходимые включения и удалит ненужные.

person Sergey Prigogin    schedule 10.10.2013

Обычно я создаю один исходный файл (например, main.c) и один файл заголовка для этого исходного файла (main.h). В исходный файл я помещаю все основные виды «интерфейсных» функций, которые я использую в этом файле (в основном это будет main()), а затем все функции, которые я получаю после рефакторинга этих функций (детали реализации), перейдите ниже . В файле заголовка я объявляю некоторые extern функции, которые определены в других исходных файлах, но используются в исходном файле, который использует этот заголовок. Затем я объявляю любые структуры или другие типы данных, которые я использую в этом исходном файле.

Затем я просто компилирую и связываю их все вместе. Он остается красивым и чистым. Типичный раздел include ... в моем текущем проекте выглядит так

#include<windows.h>
#include<windowsx.h>
#include<stdio.h>

#include"interface.h"
#include"thissourcefile.h"

//function prototypes

//source

есть заголовок interface, который отслеживает структуры данных, которые я использую во всех формах проекта, а затем thissourcefile.h, который делает именно то, что я только что объяснил (объявляет externs и т. д.).

Кроме того, я никогда ничего не определяю в своих заголовках, я только помещаю туда объявления. Таким образом, они могут быть включены в разные исходные файлы и при этом успешно связываться. Прототипы функций (внешние, статические или другие) и объявления помещаются в заголовок, поэтому их можно использовать много раз - определения находятся в исходном коде, потому что они должны находиться только в одном месте.

Очевидно, все было бы иначе, если бы вы создавали библиотеку или что-то в этом роде. Но я считаю, что это позволяет сохранить все в порядке и чистоте только для внутренней компоновки проекта. Кроме того, если вы напишете make-файл (или просто используете IDE), то компиляция станет действительно простой и эффективной.

person Carson Myers    schedule 18.06.2009