是否需要定义所有前向声明?

Is it required to define all forward declarations?

总的来说,我想知道像这样包含从未定义的 class 的前向声明的程序在技术上是否合式?

class X;
int main() {}

更具体地说,我想知道是否有这样的模式

// lib.h
#pragma once

struct X {
private:
  friend class F;
};
如果 lib.h 属于不包含 class F 的定义的共享库,则

是安全的,也不依赖于另一个包含定义的共享库。

是否有人使用头文件以引用符号 F 结束,这可能会导致加载共享库时出现链接器错误?

是的,代码是 well-formed。

您没有以需要 X 才能完成的方式使用 X。 仅当您尝试创建对象、使用成员或任何需要定义的不完整类型时,才会出现编译器错误。

不完整的类型没关系,它们只是不……完整。通常你需要的类型只是一个声明,而定义并不重要。


举个例子,考虑一个标记类型,一个模板化类型,其模板参数仅用于从模板创建不同的类型:

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

标签tagAtagB不需要定义。它们仅用作标记,用于区分 ABAB 基本上是同一种类型,但是标签使它们成为不同的类型。

根据标准[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);

关键部分是odr-used,它是由代码中所有其他可能使用该函数的地方决定的。如果它没有在任何(可能被评估的)表达式中命名,它就不是 odr-used 并且不需要定义。

进一步:

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).

声明函数不需要定义它。调用它、获取它的地址或任何其他需要知道函数位置的表达式(获取它的地址或调用它)需要该函数存在,否则 linker 将不存在能够 link 那些用于定义的用途。如果没有用处,则不依赖于这些符号。

同样,对于 classes,同样的推理适用。同样,来自标准:

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.

[例子:下面完整的翻译单元是well-formed, 即使它从未定义 X:

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

—结束例子]

为了完整起见,标准给出了 class 类型 T 需要完整的原因:

  • 定义了类型 T 的对象
  • a non-static class 声明了类型 T 的数据成员
  • T 用作 new-expression
  • 中分配的类型或数组元素类型
  • lvalue-to-rvalue 转换应用于引用类型 T
  • 的对象的左值
  • 表达式被(隐式或显式)转换为类型 T
  • 不是空指针常量且类型不是 cv void* 的表达式,使用标准转换 dynamic_cast 转换为指向 T 的类型指针或对 T 的引用 static_cast
  • a class 成员访问运算符应用于类型 T
  • 的表达式
  • typeid 运算符或 sizeof 运算符应用于类型 T 的操作数
  • 定义或调用了 return 类型或参数类型为 T 的函数
  • 定义了一个 class,其基数 class 类型为 T
  • 类型 T 的左值分配给
  • 类型 T 是 alignof 表达式的主题
  • exception-declaration 具有类型 T、对 T 的引用或指向 T