Столкновение с именем класса С++

Для следующего кода 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- или времени выполнения (сейчас или в какой-то момент в будущем).