【问题标题】:Am I guaranteed to not be bitten by this ODR violation?我能保证不会被这种 ODR 违规行为所困扰吗?
【发布时间】:2025-12-07 06:55:01
【问题描述】:

我有一个头文件,它声明了一个带有静态变量的模板并定义了它:

/* my_header.hpp */
#ifndef MY_HEADER_HPP_
#define MY_HEADER_HPP_

#include <cstdio>

template<int n>
struct foo {
    static int bar;

    static void dump() { printf("%d\n", bar); }
};

template<int n>
int foo<n>::bar;

#endif // MY_HEADER_HPP_

main.cpp 和共享库 mylib 都包含此标头。特别是,mylib_baz.hpp 仅包含此模板并声明了一个修改模板特化的函数。

/* mylib_baz.hpp */
#ifndef MYLIB_BAZ_HPP_
#define MYLIB_BAZ_HPP_

#include "my_header.hpp"

typedef foo<123> mylib_foo;
void init_mylib_foo();

#endif // MYLIB_BAZ_HPP_

/* mylib_baz.cpp */
#include "mylib_baz.hpp"
void init_mylib_foo() {
    mylib_foo::bar = 123;
    mylib_foo::dump();
};

当我创建mylib.so(包含mylib_baz.o)时,foo&lt;123&gt;::bar 的符号出现并被声明为弱。但是,foo&lt;123&gt;::bar 的符号在我的main.o 中也被声明为弱:

/* main.cpp */
#include "my_header.hpp"
#include "mylib_baz.hpp"

int main() {
    foo<123>::bar = 456;
    foo<123>::dump(); /* outputs 456 */
    init_mylib_foo(); /* outputs 123 */
    foo<123>::dump(); /* outputs 123 -- is this guaranteed? */
}

我似乎违反了一个定义规则(foo&lt;123&gt;::barmy_header.cppmain.cpp 中都定义了)。但是,对于 g++ 和 clang,符号都被声明为弱(或唯一),所以我不会被这个所困扰——所有对 foo&lt;123&gt;::bar 的访问都会修改同一个对象。

问题 1:这是巧合(可能 ODR 对模板的静态成员的工作方式不同?)还是我实际上按照标准保证了这种行为?

问题 2:我怎么能预测我观察到的行为?也就是说,究竟是什么让编译器声明符号为弱?

【问题讨论】:

  • 我认为标准说“它不应该工作”,但您的链接器说“没关系”。违反 ODR 是一个坏主意——代码不会在任何地方都有效。在 C 中,有一个“通用扩展”允许多个定义工作(请参阅 How do I use extern to share variables between source files in C? — 该功能实际上是链接器的属性,您的 C 和 C++ 编译器很有可能共享链接器技术。)
  • 同意!我也很确定这不应该起作用,但我不能真正说出为什么。在我的情况下,我可以(并且应该!)声明 bar extern 并且只在 mylib_baz.cpp 中定义它,解决了这个问题。但我真的很想深入挖掘并了解我观察到的行为背后的基本原理。
  • "foo&lt;123&gt;::bar 在 my_header.cpp 和 main.cpp 中都定义了"... 什么?我看到它定义在一个地方,这两个地方都不是你提到的:my_header.hpp。这是一个定义。
  • @Barry,标头包含在两个 .cpp 文件中(通过mylib_baz.hpp),然后分别编译。因此,在链接之前,两个 .o 文件中都有定义
  • 问题是:如果你在一个编译单元中改变它,你是否总是在另一个编译单元中观察它?

标签: c++ templates linker one-definition-rule


【解决方案1】:

没有违反 ODR。您对bar一个 定义。在这里:

template<int n>
int foo<n>::bar; // <==

由于barstatic,这表明在所有翻译单元中都有一个 定义。即使bar 将在所有目标文件中出现一次(毕竟它们需要一个符号),链接器会将它们合并在一起成为bar 的一个真正定义。你可以看到:

$ g++ -std=c++11 -c mylib_baz.cpp -o mylib_baz.o
$ g++ -std=c++11 -c main.cpp -o main.o
$ g++ main.o mylib_baz.o -o a.out

生产:

$ nm mylib_baz.o | c++filt | grep bar
0000000000000000 u foo<123>::bar
$ nm main.o | c++filt | grep bar
0000000000000000 u foo<123>::bar
$ nm a.out | c++filt | grep bar
0000000000601038 u foo<123>::bar

u 表示:

“你”
该符号是唯一的全局符号。这是标准 ELF 符号绑定集的 GNU 扩展。对于这样的符号,动态链接器将确保在整个过程中只有一个具有此名称和类型的符号在使用中。

【讨论】:

  • static 并不是造成这种情况的原因,确切地说。 bar 是模板的成员,可以与该模板一起实例化(并在翻译单元之间统一)。
最近更新 更多