对于以下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中创建了foo.cc中定义的X的实例。但是,在调用call时,它不会分派到foo.cc中定义的hook()而是分派到bar.cc中定义的hook()。这会导致损坏,因为钩子的参数仍然是int,而不是double。
可以通过将X的定义放在foo.cc中的X的定义而不是在bar.cc中的X定义中来解决该问题
最后是问题:没有关于此的编译器警告。 gcc,clang或MSVC ++都没有显示警告。按照C ++标准的定义,该行为是否有效?
这种情况似乎有点虚构,但它发生在现实世界中。我正在使用Rapidcheck编写测试,其中将要测试的单元上的可能操作定义为类。 大多数容器类具有类似的操作,因此在编写队列和向量的测试时,名称如“清除”,“推送”或“弹出”的类可能会出现几次。由于这些仅在本地需要,因此我将它们直接放在执行测试的源中。
答案 0 :(得分:48)
该程序格式错误,因为它通过对类X
具有两个不同的定义而违反了One-Definition Rule。因此,它不是有效的C ++程序。请注意,该标准专门允许编译器不诊断这种冲突。因此,编译器是一致的,但是程序不是有效的C ++,因此在执行时具有Undefined Behaviour(因此可能发生任何事情)。
答案 1 :(得分:25)
在不同的编译单元中,您有两个名称相同但不同的类X
,导致程序格式错误,因为现在有两个符号相同。由于只能在链接期间检测到该问题,因此编译器无法(也不需要)报告此问题。
避免这种情况的唯一方法是放置任何不打算导出的代码 (尤其是所有尚未在头文件中声明的代码)放入anonymous or unnamed namespace:
#include "foo.h"
#include <iostream>
#include "def.h"
namespace {
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.cc
。实际上,这是未命名名称空间的主要(唯一的)目的。
在实践中,简单地重命名类(例如fooX
和barX
)可能对您有用,但这不是一个稳定的解决方案,因为不能保证这些符号名称不会被在链接或运行时(现在或将来的某个时刻)加载的一些晦涩的第三方库。