Столкновение с именем класса С++
Для следующего кода C++ я получаю неожиданное поведение. Поведение было проверено с недавними GCC, Clang и MSV C++. Чтобы вызвать его, требуется разделить код на несколько файлов.
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 создается экземпляр X, определенный внутри foo.cc, и через вызов call() вызывается реализация hook() внутри foo.cc. То же самое для бара.
Что на самом деле происходит:
Экземпляр X, определенный в foo.cc, выполнен в foo(). Но при вызове вызова отправляется не на hook(), определенную в foo.cc, а на hook(), определенную в bar.cc. Это приводит к коррупции, поскольку аргумент для привязки по-прежнему является int, а не двойным.
Проблема может быть решена путем размещения определения X внутри foo.cc в другом пространстве имен, кроме определения X внутри bar.cc
Итак, наконец, вопрос: об этом нет предупреждений компилятора. Ни gcc, ни clang, ни MSV C++ даже не предупредили об этом. Является ли это поведение действительным, как определено в стандарте C++?
Ситуация, похоже, немного сконструирована, но это произошло в реальном мире. Я писал тесты с быстрым чеком, где возможные действия над тестируемым модулем определяются как классы. Большинство классов контейнеров имеют схожие действия, поэтому при написании тестов для очереди и вектора классы с именами типа "Clear", "Push" или "Pop" могут появляться несколько раз. Поскольку они требуются только локально, я помещаю их непосредственно в источники, где выполняются тесты.
Ответы
Ответ 1
Программа является неправильной, потому что она нарушает одноосное правило, имея два разных определения для класса X
Таким образом, это не действительная C++ программа. Обратите внимание, что стандарт специально позволяет компиляторам не диагностировать это нарушение. Таким образом, компиляторы соответствуют, но программа недействительна C++ и как таковая имеет Undefined Behavior при выполнении (и, следовательно, все может случиться).
Ответ 2
У вас есть два одинаково названных, но разных класса 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();
}
и эквивалентно для bar.cc
На самом деле, это основная (единственная?) Цель неназванных пространств имен.
Простое повторное присвоение имен вашим классам (например, fooX
и barX
) может работать на вас на практике, но не является стабильным решением, поскольку нет гарантии, что эти имена символов не используются какой-то неясной сторонней библиотекой, загруженной в link- или времени выполнения (сейчас или в какой-то момент в будущем).