【发布时间】:2018-12-11 18:21:36
【问题描述】:
对于以下 C++ 代码,我遇到了意外行为。该行为已通过最近的 GCC、Clang 和 MSVC++ 进行了验证。要触发它,需要将代码拆分到多个文件中。
def.h
#pragma once
template<typename T>
struct Base
{
void call() {hook(data);}
virtual void hook(T& arg)=0;
T data;
};
foo.h
#pragma once
void foo();
foo.cc
#include "foo.h"
#include <iostream>
#include "def.h"
struct X : Base<int>
{
virtual void hook(int& arg) {std::cout << "foo " << arg << std::endl;}
};
void foo()
{
X x;
x.data=1;
x.call();
}
bar.h
#pragma once
void bar();
bar.cc
#include "bar.h"
#include <iostream>
#include "def.h"
struct X : Base<double>
{
virtual void hook(double& arg) {std::cout << "bar " << arg << std::endl;}
};
void bar()
{
X x;
x.data=1;
x.call();
}
main.cc
#include "foo.h"
#include "bar.h"
int main()
{
foo();
bar();
return 0;
}
预期输出:
foo 1
bar 1
实际输出:
bar 4.94066e-324
bar 1
我预期会发生什么:
在 foo.cc 内部,创建了一个定义在 foo.cc 中的 X 的实例,并通过调用 call(),调用了 foo.cc 中的 hook() 的实现。酒吧也一样。
实际发生了什么:
在 foo.cc 中定义的 X 实例是在 foo() 中创建的。但是在调用 call 时,它不会调度到 foo.cc 中定义的 hook(),而是调度到 bar.cc 中定义的 hook()。这会导致损坏,因为 hook 的参数仍然是 int,而不是 double。
可以通过将 foo.cc 中的 X 定义放在 bar.cc 中的 X 定义之外的其他命名空间中来解决问题
所以最后的问题是:没有编译器警告。 gcc、clang 或 MSVC++ 都没有对此发出警告。按照 C++ 标准的定义,这种行为是否有效?
这种情况似乎有点虚构,但它发生在真实世界的场景中。我正在使用 rapidcheck 编写测试,其中对要测试的单元的可能操作被定义为类。 大多数容器类都有类似的操作,因此在为队列和向量编写测试时,可能会多次出现名称为“Clear”、“Push”或“Pop”的类。由于这些仅在本地需要,我已将它们直接放入执行测试的源中。
【问题讨论】:
-
为避免违反 ODR,请将
X移动到匿名命名空间中(这将为它们提供一个唯一但隐藏的名称)。 -
是的,这是有效的行为。该标准将 ODR 违规视为电梯中的屁,无需诊断。第 3.2 章第 4 节明确指出。不可能是编译错误,需要在链接时检测到。但是链接器永远是薄弱环节,它们不够聪明,无法检测到这个问题。
-
进行统一/巨型构建和/或 LTO/WPO 可能会触发警告。
-
@Acorn 统一构建(这是可憎的)会给出编译器错误而不是链接器错误,因为它甚至不是 ODR 违规,只是统一构建中格式错误的重新声明。 LTO 可以对此进行诊断是对的,例如GCC 将使用
-flto -Wodr对其进行诊断
标签: c++