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

发布于 2025-01-09 03:19:27 字数 351 浏览 3 评论 0原文

一般来说,我想知道像这样的程序(包含从未定义的类的前向声明)在技术上是否格式良好?

class X;
int main() {}

这样的模式是否

// lib.h
#pragma once

struct X {
private:
  friend class F;
};

更具体地说,我想知道如果 lib.h 属于不包含类 F 定义的共享库,那么编写 安全,也不依赖于另一个共享库。

使用头文件的人是否有可能最终引用符号 F ,这可能会在加载共享库时导致链接器错误?

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?

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(2

魂牵梦绕锁你心扉 2025-01-16 03:19:27

根据标准[basic.odr.def]:

每个程序都应包含每个非内联的确切定义
在该程序外部使用的函数或变量
丢弃的语句(8.5.1);

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

更远:

如果函数是唯一的,则该函数由表达式或转换命名
名称查找的结果或一组重载的选定成员
重载决策中的函数(6.5、12.4、12.5)执行为
形成该表达式或转换的一部分
,除非它是纯粹的
虚函数且表达式不是 id 表达式
使用显式限定名称或
表达式形成一个指向成员的指针 (7.6.2.1)。

声明函数不需要定义它。调用它、获取其地址或任何其他需要知道函数位置的表达式(获取其地址或调用它)都需要该函数存在,否则链接器将无法链接这些用途到定义。如果没有用途,则不存在对这些符号的依赖性。

同样,对于类,同样的推理也适用。再次,从标准来看:

类的定义需要在每个上下文中都可访问
其中类的使用方式要求类类型是
完成。

[示例:以下完整的翻译单元格式良好,
即使它从未定义 X:

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

— 结束示例]

为了完整性,标准给出的类类型 T 需要完整的原因是:

  • 定义了类型 T 的对象、
  • 类型 T 的非静态类数据成员是声明的
  • T 用作 new 表达式中的分配类型或数组元素类型
  • 左值到右值转换应用于引用 T 类型对象的左值
  • 表达式被转换(隐式或显式)为 T
  • 类型表达式不是空指针常量,并且具有除 cv void* 之外的类型,使用标准转换、dynamic_cast 或 a 转换为指向 T 的类型指针或对 T 的引用
    static_cast
  • 将类成员访问运算符应用于 T 类型的表达式
  • 将 typeid 运算符或 sizeof 运算符应用于 T 类型的操作数
  • 返回类型或参数类型为 T 类型的函数 定义或称为
  • 具有基类的类定义类型 T 的类 类型
  • T 的左值被分配给
  • 类型 T 是alignof 表达式的主题
  • 异常声明具有类型 T、对 T 的引用或指向 T 的指针

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
善良天后 2025-01-16 03:19:27

是的,代码格式良好。

您没有以需要 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 基本上是相同的类型,但标签使它们成为不同的类型。

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.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文