What are the dangers of forward declarations?

2020-05-19 05:00发布

问题:

I just had an interview. I was asked what is a "forward declaration". I was then asked if they were dangers associated with forward declarations.

I could not answer to the second question. A search on the net didn't show up any interesting result.

So, do someone know any dangers associated with the use of forward declarations ?

回答1:

Forward declaration is the symptom of C++ missing modules (going to be fixed in C++17?) and using headers inclusion, if C++ had modules there were no need at all for forward declarations.

A forward declaration is not less than a "contract", by using it you actually promise that you will provide the implementation of something (after in the same source file, or by linking a binary later).

The cons of that is that you actually have to follow your contract, not much very an issue because if you don't follow your contract the compiler will somehow complain early, but in some languages code just get executed without the need to "promise its own existence" (speaking of dynamically typed languages)



回答2:

Well, apart from the issues about duplication...

... there is at least one sore spot in the Standard.

If you call delete on a pointer to an incomplete type, you get undefined behavior. In practice, the destructor may not get called.

We can see that on LiveWorkSpace using the following command and sample:

// -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);
}

Diagnosis:

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

Don't you want to thank your compiler for warning you ;) ?



回答3:

I'd say any danger is eclipsed by the gains. There are some though, mostly related to refactoring.

  • renaming classes impacts all forward-declarations. This of course also comes with includes, but the error is generated in a different place, so harder to spot.
  • moving classes from a namespace to another, coupled with using directives, can wreak havoc (mysterious errors, hard to spot and fix) - of course, the using directives are bad to start off with, but no code is perfect, right?*
  • templates - to forward declare templates (esp. user-defined ones) you'll need the signature, which leads to code duplication.

Consider

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
{};

The class Z was changed afterwards as the default template argument, but the original forward declaration is still with int.

*I've recently ran into this:

Original:

Definition:

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

and forward declaration:

//my code
namespace A { struct X; }

After refactoring:

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

This obviously rendered my code invalid, but the error wasn't at the actual place and the fix was, to say the least, fishy.



回答4:

If a pointer to incomplete class type is passed to delete, an operator delete overload may be overlooked.

That's all I got… and to be bitten, you would have to do that yet nothing else in the source file that would cause an "incomplete type" compiler error.

EDIT: Following the lead of the other guys, I'd say the difficulty (may be considered danger) is ensuring that the forward declaration, in fact matches the real declaration. For functions and templates, argument lists have to be kept in sync.

And you need to delete the forward declaration when removing the thing it declares, or it sits around and crufts up the namespace. But even in such cases, the compiler will point at it in the error messages if it gets in the way.

The bigger danger is not having a forward declaration. A major disadvantage of nested classes is that they cannot be forward declared (well, they can inside the enclosing class scope, but that's only brief).



回答5:

The only danger of forward-declaring something is when you do the forward declaration outside of a header or in a non-shared header, and the signature of the forward declaration differs from the actual signature of whatever being forward-declared. If you do it in extern "C", there would be no name mangling to check the signature at link time, so you may end up with undefined behavior when the signatures do not match.



回答6:

Another danger of forward declarations is that it makes it easier to violate the One Definition Rule. Assuming that you have a.h forward declaring class B (which is supposed to be in b.h and b.cpp), but inside a.cpp you actually include b2.h which declares a different class B than b.h, then you get to undefined behaviour.



回答7:

A forward declaration is not so much dangerous in itself, but it is a code smell. If you need a forward declaration, it means two classes are tightly coupled, which usually is bad. Thus it is an indication that your code may need refactoring.

There are some cases where it is OK to have tight coupling, e.g. the concrete states in a state pattern implementation may be tightly coupled. I would consider this OK. But in most other cases, I would improve my design before using a forward declaration.



回答8:

I came across an interesting snippet in the Google C++ Style Guide

The danger they point to arises from implementing functions on incomplete types. Normally, this the compiler would throw an error, but because these are pointers, it can slip through the net.

It can be difficult to determine whether a forward declaration or a full #include is needed. Replacing an #include with a forward declaration can silently change the meaning of code:

// 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*)

If the #include was replaced with forward decls for B and D, test() would call f(void*).



回答9:

The first way is to reorder our function calls so add is defined before main:

That way, by the time main() calls add(), it will already know what add is. Because this is such a simple program, this change is relatively easy to do. However, in a large program, it would be extremely tedious trying to decipher which functions called which other functions so they could be declared in the correct order.