2012-03-28 2 views
57

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

//file C.h 
#include "A.h" 
#include "B.h" 

class C{ 
    A* a; 
    B b; 
    ... 
}; 

сделать это вместо того, чтобы:

//file C.h 
#include "B.h" 

class A; 

class C{ 
    A* a; 
    B b; 
    ... 
}; 


//file C.cpp 
#include "C.h" 
#include "A.h" 
... 

Есть ли причина, почему не делать это везде, где это возможно?

+0

простой ответ, нет. – Nim

+3

Умм - это ответ на вопрос сверху или снизу? – Mat

+1

ваш реальный вопрос (внизу) - AFAIK нет причин не использовать в этом случае форвардную декларацию ... – Nim

ответ

47

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

Там нет минусов в ФОРВАРДНОМ объявляя классы, но я могу думать о некоторых недостатках для включения заголовков ненужно:

  • больше времени компиляции, так как все единицы перевода, включая C.h будут также включать A.h, хотя они может не понадобиться.

  • включая, возможно, другие заголовки вам не нужны косвенно

  • загрязняя единицы перевода с символами вам не нужно

  • вам, возможно, потребуется перекомпилировать исходные файлы, которые содержат этот заголовок, если он изменяет (@PeterWood)

+11

Кроме того, повышенная вероятность перекомпиляции. –

+8

«Я не могу придумать ситуацию, когда лучше всего включить файл, в котором вы можете использовать форвардное объявление» - когда декларация forward дает UB, см. Мой комментарий к основному вопросу. Вы правы, чтобы быть осторожным, я думаю :-) –

+0

@SteveJessop Я не знал, что вы можете это сделать. Однако это дает предупреждение. Почему бы вам не добавить это как ответ? –

28

Да, использование форвардных деклараций всегда лучше.

Некоторые из преимуществ, которые они обеспечивают, являются:

  • Сокращение времени компиляции.
  • Без пространства имен загрязнять.
  • (В некоторых случаях) может уменьшить размер сгенерированных двоичных файлов.
  • Время перекомпиляции может быть значительно уменьшено.
  • Избегайте потенциального столкновения имен препроцессора.
  • Внедрение PIMPL Idiom Таким образом, обеспечивается средство скрытия реализации от интерфейса.

Однако, вперед объявить класс делает этот конкретный класс Неполное типа и серьезно ограничивает, какие операции можно выполнять на неполном типа.
Вы не можете выполнять какие-либо операции, которые необходимы компилятору для определения макета класса.

С неполном типа вы можете:

  • Declare член как указатель или ссылку на неполный тип.
  • Объявлять функции или методы, которые принимают/возвращают неполные типы.
  • Определите функции или методы, которые принимают/возвращают указатели/ссылки на неполный тип (но не используют его элементы).

С неполном типа вы не можете:

  • использовать его в качестве базового класса.
  • Используйте это, чтобы объявить участников.
  • Определите функции или методы, используя этот тип.
+1

«Однако Forward, объявляющий класс, делает этот класс незавершенным, и это строго, ограничивает, какие операции вы можете выполнять в незавершенном типе». Ну да, но если вы * можете * переслать объявление, это означает, что вам не нужен полный тип в заголовке. И если вам нужен полный тип файла, который включает этот заголовок, просто включите заголовок для нужного вам типа. IMO, это преимущество - оно заставляет вас включать все, что вам нужно в ваш файл реализации, и не полагаться на то, что оно включено в другое место. –

+0

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

+1

@LuchianGrigore: * .. но если вы можете переадресовать объявить его ... *, вам придется попробовать его проверить. Так что нет фиксированного правила, чтобы просто перейти на Forward Declarations & not include headers, зная, что правила помогают оторвав ваши реализации.Наиболее распространенное использование деклараций Forward состоит в том, чтобы разбить круговые зависимости, и это то, что * То, что вы не можете делать с незавершенными типами *, обычно укусывает вас. Каждый исходный файл и заголовочный файл должны содержать все заголовки, необходимые для компиляции, поэтому второй аргумент не применяется. Для начала это просто плохо организованный код. –

1

Есть ли причина, по которой это невозможно сделать, когда это возможно?

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

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

+6

Именование всех ваших переменных 'a',' b', ...., 'a1',' a2' также экономит при вводе текста , –

+0

@ Luchian Grigore: возможно, это нормально для какой-то простой тестовой программы. – ks1322

14

Есть ли причина, по которой это невозможно сделать, когда это возможно?

Удобство.

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

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

+3

Это единственный ответ, указывающий на то, что он имеет такую ​​производительность. +1 – usr

+0

С точки зрения пользователя. Если вы пересылаете объявление всем, это означает, что пользователь не может просто включить этот файл и сразу начать работать. Они должны выяснить, каковы зависимости (вероятно, из-за компилятора, жалующегося на неполные типы), а также включить эти файлы, прежде чем они смогут начать использовать ваши классы. Альтернативой является создание файла shared.hpp или что-то для вашей библиотеки, где все заголовки находятся в этом файле (например, boost, упомянутый выше). Они могут легко включить его, не выясняя, почему они не могут просто «включать и уходить». – Todd

8

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

// Forward declarations 
template <typename A> class Frobnicator; 
template <typename A, typename B, typename C = Frobnicator<A> > class Gibberer; 

// Alternative: more clear to the reader; more stable code 
#include "Gibberer.h" 

// Declare a function that does something with a pointer 
int do_stuff(Gibberer<int, float>*); 

ФОРВАРДНОЙ декларация такие же, как дублирование кода: если код имеет тенденцию изменяться много, вы должны изменить его в 2 места или больше каждый раз, и это нехорошо.

+2

+1 для разрушения консенсуса в том, что форвардная декларация строго всегда лучше :-) IIRC та же проблема возникает с типами, которые являются «секретными» экземплярами шаблонов через typedefs. 'namespace std {class string; } 'было бы неправильным, даже если бы было разрешено помещать объявление класса в пространство имен std, потому что (я думаю) вы не можете юридически переслать - объявить typedef, как если бы это был класс. –

0

Есть ли причина, по которой это невозможно сделать, когда это возможно?

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

На моем компьютере функции Быстрее() проходит около 2000X быстрее, чем функция более медленной():

class SomeClass 
{ 
public: 
    void DoSomething() 
    { 
     val++; 
    } 
private: 
    int val; 
}; 

class UsesPointers 
{ 
public: 
    UsesPointers() {a = new SomeClass;} 
    ~UsesPointers() {delete a; a = 0;} 
    SomeClass * a; 
}; 

class NonPointers 
{ 
public: 
    SomeClass a; 
}; 

#define ARRAY_SIZE 100000 
void Slower() 
{ 
    UsesPointers list[ARRAY_SIZE]; 
    for (int i = 0; i < ARRAY_SIZE; i++) 
    { 
     list[i].a->DoSomething(); 
    } 
} 

void Faster() 
{ 
    NonPointers list[ARRAY_SIZE]; 
    for (int i = 0; i < ARRAY_SIZE; i++) 
    { 
     list[i].a.DoSomething(); 
    } 
} 

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

Это хорошее представление о предмете и других факторов эффективности: http://research.scee.net/files/presentations/gcapaustralia09/Pitfalls_of_Object_Oriented_Programming_GCAP_09.pdf

+9

Вы отвечаете на другой вопрос («Должен ли я использовать указатели?»), А не тот, который был задан («Когда я использую только указатели, есть ли причина не использовать форвардные объявления?»). –

6

Если один использовать вперед деклараций вместо включает в себя везде, где это возможно?

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

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

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

Вот некоторые примеры «невидимых рисков» относительно вперед декларации (невидимые риски = декларации несоответствия, которые не обнаруженных компилятором или линкера):

  • Явные деклараций вперед символов, представляющих данные могут быть небезопасными , так как для таких форвардных объявлений может потребоваться правильное знание размера (размера) данных.

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

В приведенном ниже примере показано, это, например, два опасных декларации вперед данных, а также функции:

переменного файла:

#include <iostream> 
char data[128][1024]; 
extern "C" void function(short truncated, const char* forgotten) { 
    std::cout << "truncated=" << std::hex << truncated 
      << ", forgotten=\"" << forgotten << "\"\n"; 
} 

Ьс Файл:

#include <iostream> 
extern char data[1280][1024];   // 1st dimension one decade too large 
extern "C" void function(int tooLarge); // Wrong 1st type, omitted 2nd param 

int main() { 
    function(0x1234abcd);       // In worst case: - No crash! 
    std::cout << "accessing data[1270][1023]\n"; 
    return (int) data[1270][1023];    // In best case: - Boom !!!! 
} 

Компиляция программы с g ++ 4.7.1:

> g++ -Wall -pedantic -ansi a.c b.c 

Примечание: Invisible опасность, поскольку г ++ не дает никакой компилятор или компоновщик ОШИБКИ/предупреждения
Примечание: Опуская extern "C" приводит к связующей погрешности для function() в связи с C++ имя коверкая.

Запуск программы:

> ./a.out 
truncated=abcd, forgotten="♀♥♂☺☻" 
accessing data[1270][1023] 
Segmentation fault 
5

Забавный факт, in its C++ styleguide, Google recommands используя #include везде, но, чтобы избежать циклических зависимостей.

2

Есть ли причина, по которой это невозможно сделать, когда это возможно?

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

вперед объявление функции:

  • требует, зная, что он реализован в виде функции, а не экземпляр статического объекта функтора или (удушье!) Макрос,

  • требует дублирования по умолчанию значения для параметров по умолчанию,

  • требует знать свое фактическое имя и пространство имен, так как это может быть только объявление using, которое вытаскивает его в другое пространство имен, возможно, под псевдоним, и

  • может потерять при встроенной оптимизации.

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

вперед объявить класс:

  • требует знать, является ли это производный класс и базовый класс (а) это происходит от,

  • требует зная, что это класс, а не только ЬурейеЕ или конкретное создание шаблона класса (или знание того, что это шаблон класса, и правильная настройка всех параметров шаблона и значений по умолчанию),

  • требует знать истинное имя и пространство имен t он класс, так как это может быть объявление using, которое тянет его в другое пространство имен, возможно, под псевдонимом, и

  • требует знания правильных атрибутов (возможно, у него есть особые требования к выравниванию).

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

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

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

Смежные вопросы