对于以下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;
};
Run Code Online (Sandbox Code Playgroud)
foo.h中
#pragma once
void foo();
Run Code Online (Sandbox Code Playgroud)
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();
}
Run Code Online (Sandbox Code Playgroud)
bar.h
#pragma once
void bar();
Run Code Online (Sandbox Code Playgroud)
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();
}
Run Code Online (Sandbox Code Playgroud)
main.cc
#include "foo.h"
#include "bar.h"
int main()
{
foo();
bar();
return 0;
}
Run Code Online (Sandbox Code Playgroud)
预期产量:
foo 1
bar 1
Run Code Online (Sandbox Code Playgroud)
实际产量:
bar 4.94066e-324
bar 1
Run Code Online (Sandbox Code Playgroud)
我期待发生的事情:
在foo.cc中,在foo.cc中定义了一个X实例,并通过调用call(),调用foo.cc中的hook()实现.酒吧也一样.
实际发生了什么:
foo.cc中定义的X实例是在foo()中创建的.但是当调用call时,它不会调度到foo.cc中定义的hook(),而是调度bar.cc中定义的hook().这会导致损坏,因为hook的参数仍然是int,而不是double.
通过将f的定义放在foo.cc中的另一个命名空间而不是bar.cc中的X定义,可以解决这个问题
最后问题是:没有关于此的编译器警告.gcc,clang或MSVC++都没有对此发出警告.该行为是否按照C++标准定义有效?
情况似乎有点构建,但它发生在现实世界的场景中.我正在使用rapidcheck编写测试,其中对要测试的单元的可能操作被定义为类.大多数容器类都有类似的操作,因此在为队列和向量编写测试时,名称如"Clear","Push"或"Pop"的类可能会多次出现.由于这些仅在本地需要,我将它们直接放在执行测试的源中.
Rei*_*ica 48
该程序是错误的,因为它通过对类有两个不同的定义来违反单一定义规则X
.所以它不是一个有效的C++程序.请注意,该标准特别允许编译器不诊断此违规.所以编译器是符合要求的,但程序是无效的C++ ,因此在执行时具有未定义的行为(因此任何事情都可能发生).
Wal*_*ter 25
X
在不同的编译单元中有两个相同名称但不同的类,导致程序格式错误,因为现在有两个具有相同名称的符号.由于只能在链接期间检测到问题,因此编译器无法(并且不需要)报告此问题.
避免此类事情的唯一方法是将任何 不打算导出的代码(特别是所有尚未在头文件中声明的代码)放入匿名或未命名的命名空间中:
#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();
}
Run Code Online (Sandbox Code Playgroud)
并且相当于bar.cc
.实际上,这是未命名命名空间的主要(唯一?)目的.
简单地重新命名您的类(例如fooX
和barX
)可能在实践中对您有用,但不是一个稳定的解决方案,因为无法保证这些符号名称不被链接或运行时加载的某些不起眼的第三方库使用时间(现在或将来的某个时间点).