Чем опасны форвардные объявления?

У меня только что было интервью. Меня спросили, что такое «форвардная декларация». Затем меня спросили, опасны ли они, связанные с предварительными объявлениями.

На второй вопрос я не смог ответить. Поиск в сети ничего интересного не дал.

Итак, кто-нибудь знает какие-либо опасности, связанные с использованием предварительных объявлений?


person Offirmo    schedule 17.01.2013    source источник
comment
Нет никаких опасностей, просто предварительное объявление типа делает этот тип неполным типом для компилятора, что ограничивает возможности использования этого типа в конкретной TU. Хотя это ни в коем случае не ограничение.   -  person Alok Save    schedule 17.01.2013
comment
@thang, дающий такой ответ на собеседовании, обязательно наймет тебя на работу :P   -  person Dariusz    schedule 17.01.2013


Ответы (9)


Упреждающее объявление — это симптом отсутствующих модулей C++ (что будет исправлено в C++17?) и использования включения заголовков, если бы у C++ были модули, то не было бы необходимости в упреждающих объявлениях.

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

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

person CoffeDeveloper    schedule 17.11.2016

Ну, кроме проблем с дублированием...

... в Стандарте есть как минимум одно больное место.

Если вы вызываете delete для указателя на неполный тип, вы получаете неопределенное поведение. На практике деструктор может не вызываться.

Мы можем увидеть это на LiveWorkSpace, используя следующую команду и пример:

// -std=c++11 -Wall -W -pedantic -O2

#include <iostream>

struct ForwardDeclared;

void throw_away(ForwardDeclared* fd) {
   delete fd;
}

struct ForwardDeclared {
   ~ForwardDeclared() {
      std::cout << "Hello, World!\n";
   }
};

int main() {
   ForwardDeclared* fd = new ForwardDeclared();
   throw_away(fd);
}

Диагноз:

Compilation finished with warnings:
 source.cpp: In function 'void throw_away(ForwardDeclared*)':
 source.cpp:6:11: warning: possible problem detected in invocation of delete operator: [enabled by default]
 source.cpp:5:6: warning: 'fd' has incomplete type [enabled by default] 
 source.cpp:3:8: warning: forward declaration of 'struct ForwardDeclared' [enabled by default]
 source.cpp:6:11: note: neither the destructor nor the class-specific operator delete will be called, even if they are declared when the class is defined

Разве вы не хотите поблагодарить свой компилятор за предупреждение;)?

person Matthieu M.    schedule 17.01.2013
comment
принято всенародным голосованием, спасибо! Читатели также должны взглянуть на другие ответы. - person Offirmo; 18.01.2013
comment
@Offirmo: не чувствую никакого давления, чтобы принять какой-либо ответ. Это ваш вопрос, и вы должны принимать ответ только в том случае, если он вас удовлетворил, и вы, конечно же, не обязаны принимать тот, который набрал наибольшее количество голосов. Твоя проблема, твои правила. - person Matthieu M.; 18.01.2013
comment
Я не правильно выразился. Этот вопрос открытый, не направленный на конкретную проблему. Ваш ответ очень хорош и обрисовывает в общих чертах то, что кажется самой большой опасностью (большинство людей согласны). Значит, ты заслуживаешь быть ответом. Теперь интересны также случаи редких шаблонов Лучиана, и их следует прочитать, чтобы получить полный ответ. Я не могу выбрать несколько ответов, поэтому, пожалуйста, все проголосуйте за ответ Лучиана вместе с этим. - person Offirmo; 19.01.2013
comment
@Offirmo: тогда давайте проголосуем за ответ Лучиана, чтобы он приблизился к вершине;) - person Matthieu M.; 19.01.2013

Я бы сказал, что любая опасность затмевается прибылью. Однако есть некоторые, в основном связанные с рефакторингом.

  • переименование классов влияет на все предварительные объявления. Это, конечно, также включает в себя, но ошибка генерируется в другом месте, поэтому ее труднее обнаружить.
  • перемещение классов из namespace в другой в сочетании с директивами using может привести к хаосу (загадочные ошибки, которые трудно обнаружить и исправить) — конечно, директивы using плохи для начала, но нет идеального кода, верно?*
  • шаблоны — для пересылки шаблонов объявлений (особенно пользовательских) вам потребуется подпись, что приводит к дублированию кода.

Рассмотреть возможность

template<class X = int> class Y;
int main()
{
    Y<> * y;
}

//actual definition of the template
class Z
{  
};
template<class X = Z> //vers 1.1, changed the default from int to Z
class Y
{};

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

* Недавно я столкнулся с этим:

Оригинал:

Определение:

//3rd party code
namespace A  
{
   struct X {};
}

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

//my code
namespace A { struct X; }

После рефакторинга:

//3rd party code
namespace B
{
   struct X {};
}
namespace A
{
   using ::B::X;
}

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

person Luchian Grigore    schedule 17.01.2013
comment
"нанести ущерб", а не "опасность крушения" - person Konrad Rudolph; 17.01.2013
comment
Открытие стороннего пространства имен и добавление предварительных объявлений для их классов — это не то, о чем я бы упоминал в интервью. - person Potatoswatter; 17.01.2013

Если указатель на неполный тип класса передается в delete, перегрузка operator delete может быть пропущена.

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

РЕДАКТИРОВАТЬ: Следуя примеру других парней, я бы сказал, что трудность (может считаться опасной) заключается в обеспечении того, чтобы предварительная декларация фактически соответствовала реальной декларации. Для функций и шаблонов необходимо синхронизировать списки аргументов.

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

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

person Potatoswatter    schedule 17.01.2013

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

person Sergey Kalinichenko    schedule 17.01.2013

Еще одна опасность предварительных объявлений заключается в том, что они упрощают нарушение правила одного определения. Предполагая, что у вас есть a.h, объявляющий class B (который должен быть в b.h и b.cpp), но внутри a.cpp вы фактически включаете b2.h, который объявляет другой class B, чем b.h, тогда вы получаете неопределенное поведение.

person Andre Kostur    schedule 17.01.2013

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

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

person EvertW    schedule 04.07.2016

Я наткнулся на интересный фрагмент в Руководстве по стилю Google C++.

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

Может быть трудно определить, требуется ли предварительное объявление или полное #include. Замена #include предварительным объявлением может незаметно изменить смысл кода:

// b.h:
struct B {};
struct D : B {};

// good_user.cc:
#include "b.h"
void f(B*);
void f(void*);
void test(D* x) { f(x); }  // calls f(B*)

Если бы #include было заменено прямыми decls для B и D, test() вызвал бы f(void*).

person MMD    schedule 15.10.2018

Первый способ — переупорядочить вызовы наших функций так, чтобы add определялся перед main:

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

person rosedoli    schedule 17.01.2013