Home > Enterprise >  Is it required to define all forward declarations?
Is it required to define all forward declarations?

Time:02-22

In general, I'm wondering if a program like this, containing a forward-declaration of a class that is never defined, is technically well-formed?

class X;
int main() {}

More specifically, I'm wondering if having a pattern like this

// lib.h
#pragma once

struct X {
private:
  friend class F;
};

is safe to write if lib.h belongs to a shared library that does not contain a definition of the class F, nor does it depend on another shared library that does.

Is it possible that someone using the header file ends up with a reference to symbol F that may cause a linker error when loading the shared library?

CodePudding user response:

According to the standard [basic.odr.def]:

Every program shall contain exactly one definition of every non-inline function or variable that is odr-used in that program outside of a discarded statement (8.5.1);

The key part is odr-used, which is determined by all the other places in code that may make use of the function. If it is not named in any (potentially evaluated) expression, it is not odr-used and does not need to have a definition.

Further:

A function is named by an expression or conversion if it is the unique result of a name lookup or the selected member of a set of overloaded functions (6.5, 12.4, 12.5) in an overload resolution performed as part of forming that expression or conversion, unless it is a pure virtual function and either the expression is not an id-expression naming the function with an explicitly qualified name or the expression forms a pointer to member (7.6.2.1).

Declaring function does not require it to be defined. Calling it, taking its address, or any other expression that needs to know the location of the function (to take its address or make a call to it) requires that the function exist, or the linker won't be able to link those uses to the definitions. If there are no uses, there is no dependency on those symbols.

Similarly, for classes, the same kind of reasoning applies. Again, from the standard:

A definition of a class is required to be reachable in every context in which the class is used in a way that requires the class type to be complete.

[Example: The following complete translation unit is well-formed, even though it never defines X:

struct X;     // declare X as a struct type
struct X* x1; // use X in pointer formation
X* x2;        // use X in pointer formation

— end example]

And for completeness, the reasons the standard gives for when a class type T is required to be complete:

  • an object of type T is defined
  • a non-static class data member of type T is declared
  • T is used as the allocated type or array element type in a new-expression
  • an lvalue-to-rvalue conversion is applied to a glvalue referring to an object of type T
  • an expression is converted (either implicitly or explicitly) to type T
  • an expression that is not a null pointer constant, and has type other than cv void*, is converted to the type pointer to T or reference to T using a standard conversion, a dynamic_cast, or a static_cast
  • a class member access operator is applied to an expression of type T
  • the typeid operator or the sizeof operator is applied to an operand of type T
  • a function with a return type or argument type of type T is defined or called
  • a class with a base class of type T is defined
  • an lvalue of type T is assigned to
  • the type T is the subject of an alignof expression
  • an exception-declaration has type T, reference to T, or pointer to T

CodePudding user response:

Yes the code is well-formed.

You are not using X in a way that requires X to be complete. Only if you try to create an object, use a member, or anything that requires the definition, of an incomplete type there will be a compiler error.

Incomplete types are fine, they are just not ... complete. Often all you need of a type is a declaration while the definition doesn't really matter.


As an example consider a tagged type, a templated type whose template argument is only present to create different types from the template:

#include <type_traits>
#include <iostream>

template <typename tag>
struct tagged_type {
     // nothing in here uses tag
     // tag is only there to make tagged_type<X> and tagged_type<Y> different types
};

struct tagA;
struct tagB;

int main() {
    using A = tagged_type<tagA>;
    using B = tagged_type<tagB>;
    std::cout << std::is_same_v<A,B>;
}

The tags tagA and tagB do not need a definition. They are only used as tags, to distinguish A and B. A and B are basically the same type, but the tag makes them different types.

  • Related