Целесообразно ли использовать extern, чтобы избежать зависимости заголовка?

Я исключил два включения заголовков в единицу перевода, используя extern. Это целесообразно?

Моя конкретная ситуация: у меня есть класс с именем ParseTree, который накапливает Token*. ParseTree* является закрытым членом Parser.

Изначально у меня были следующие строки в parse_tree.cc.

#include "parser.h"
#include "token.h"

Проанализировав свой код, я выделил две функции, которые на самом деле имели внешние зависимости, и заменил включение на следующее:

extern std::ostream& operator<<(std::ostream& out, const Token& t); // @token.h
extern bool hasPriority(const Token* p_tok1, Token* p_tok2); // @parser.h

Оба решения работают. Существуют ли какие-либо скрытые опасности, о которых я должен знать, выбирая extern вместо include?


person Jordan Gray    schedule 11.08.2011    source источник
comment
Внешняя связь используется по умолчанию для объявлений функций, поэтому extern сама по себе избыточна.   -  person CB Bailey    schedule 11.08.2011
comment
Это очень хороший момент, который я упустил из виду.   -  person Jordan Gray    schedule 11.08.2011


Ответы (3)


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

Так что нет, не используйте extern, если у вас уже есть подходящий заголовочный файл.

person Greg Hewgill    schedule 11.08.2011
comment
Кажется, люди настоятельно рекомендуют уменьшать зависимости и сводить отношения между единицами перевода к минимуму. Например, pimpl и предварительная декларация. Чем эта техника отличается от этих двух? Идиома Pimpl, в частности, имеет множество потенциальных недостатков/осложнений. - person Jordan Gray; 11.08.2011
comment
+1 за подходящий заголовочный файл. Я видел использование другого варианта в качестве оптимизации времени сборки, когда многие большие файлы заголовков были связаны одним включением. - person Merlyn Morgan-Graham; 11.08.2011
comment
@Jordan: вам не нужно менять пупырышки, когда вы обновляете истинное (одно) определение. То, что вы описали, является предварительным объявлением, и в идеале оно должно содержаться в подходящем (не расширяющем зависимости) заголовочном файле. - person Merlyn Morgan-Graham; 11.08.2011
comment
@Jordan: С Pimpl вы не собираетесь менять тип указателя pimpl. При предварительном объявлении вы вряд ли измените имя типа. Но с функциями вы можете легко изменить определение параметров на эту функцию. Это просто вопрос частоты изменений. Кроме того, люди обычно предлагают размещать объявления в заголовке, отдельном от определения этих типов. Таким образом, если вам нужны объявления, вы можете включить небольшой заголовок. - person Nicol Bolas; 11.08.2011
comment
@Merlyn: Итак, возьмите домой сообщение, если я не могу оправдать оптимизацию времени сборки, не стоит пытаться перехитрить компилятор? - person Jordan Gray; 11.08.2011
comment
@Jordan: не позволяйте кому-то другому думать за вас (если только он не является основным разработчиком компилятора). Это будет стоить, и ваша выгода может превысить ее (особенно если вы подойдете творчески к своим смягчениям — генерированию кода, отправке исправлений мейнтейнерам библиотеки и т. д.). Но брать вслепую будет действительно очень дорого :) - person Merlyn Morgan-Graham; 11.08.2011
comment
@Jordan, если вы считаете, что это целесообразно, вы можете создать новые заголовки, которые будут включены в parse_tree.cc, parser.h и token.h; но есть два правила, которые я настоятельно рекомендую не нарушать: заголовок должен быть самодостаточным (в идеале обеспечивать его наличием CU, в котором он имеет первый заголовок) и, за исключением классов, должно быть не более одного объявления, которое не является определение, CU, которому это нужно, должно включать файл, который его содержит. Минимизация количества включений целесообразна, но подчиняется этим правилам. - person AProgrammer; 11.08.2011
comment
@AProgrammer: Чтобы обобщить создание заголовка для перегрузок оператора‹‹, я бы создал файл operator_print.h, который будет содержать все прототипы перегруженных функций ‹‹ для Node, Token и ParseTree. Включу ли я operator_print.h только в token.cc, node.cc и parse_tree.cc, или я также поместил бы их в token.h, node.h и ParseTree.h? - person Jordan Gray; 12.08.2011

Оба решения работают. Существуют ли какие-либо скрытые опасности, о которых я должен знать, выбирая extern вместо include?

Да. API может меняться, и компилятор будет работать не на вас, а против вас.

Научитесь использовать компилятор в своих интересах. Нет, не только «СЕЙЧАС», я имею в виду долгосрочную перспективу. В долгосрочной перспективе вы совершаете глупые ошибки, а не тогда, когда в вашей голове все свежо. Да, память вашего мозга почему-то изменчива.

person Flavius    schedule 11.08.2011

Обычно уменьшают зависимости, объявляя классы заранее. Вот пример. Скажем, у нас есть два предварительных заголовка, parser.h и token.h, определенные следующим образом:

парсер.ч:

class Parser
{
public:
  void doFoo();
  void doBar();
  // ... lots of other stuff
};

токен.ч:

class Token
{
public:
  void doQuux();
  void doBaz();
  // ... continues for a while
};

Теперь, имея «user,h», который использует эти два, вместо того, чтобы писать так:

#include "parser.h"
#include "token.h"

void useParserAndToken( Parser &, Token & );

ты пишешь

class Parser;
class Token;

void useParserAndToken( Parser &, Token & );

Таким образом, вы в конечном итоге скажете, что есть классы Parser и Token, но то, как именно они определены, не имеет значения. Это приводит к более быстрой компиляции.

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

person dragonroot    schedule 11.08.2011