【问题标题】:C++ class name collisionC++ 类名冲突
【发布时间】: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++


【解决方案1】:

您在不同的编译单元中有两个同名但不同的类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。事实上,这是未命名命名空间的主要(唯一?)用途。

简单地重新命名你的类(例如fooXbarX)在实践中可能对你有用,但不是一个稳定的解决方案,因为不能保证这些符号名称不会被一些不起眼的第三方使用 -在链接或运行时(现在或将来某个时间)加载的派对库。

【讨论】:

  • 感谢您的建议,看起来像是解决此类问题的通用解决方案。在我看来,这应该出现在每个进行本地定义的 c++ 文件中,就像标题中的 #pragma once 一样。也许下一个 c++ 标准可以让编译器默认把这些东西放在那里:P
  • @Johannes 我同意应该警告在全局命名空间中放置符号(如果真的想这样做,应该有一种方法来抑制这样的警告)。目前,我想这只是 C++ 开发人员的常识。
  • 是的,很难记住所有这些事情。每天必须在 Python、C# 和 C++ 之间切换,有时甚至需要做一些 Java。即使在超过 10 年的 c++ 经验之后,您也会感到惊讶。通常我会不惜一切代价避免使用这样的全局定义,但对于单元测试......谁在乎......
【解决方案2】:

该程序是病态的,因为它违反了One-Definition Rule 对类X 的两个不同定义。所以它不是一个有效的 C++ 程序。请注意,该标准特别允许编译器不诊断此违规行为。所以编译器是一致的,但程序不是有效的 C++,因此在执行时有Undefined Behaviour(因此任何事情都可能发生)。

【讨论】:

  • 感谢您的回复。许多 cmets 和所有答案都指向正确的方向,遗憾的是我无法将它们全部标记为正确。此时 C++ 标准中的定义对我来说似乎非常危险,但这超出了问题的范围。可能因为模板在 C++ 中的工作方式需要采用这种方式,但现代语言至少应该抱怨这一点。
  • @Johannes 正如我所解释的,编译器(= language无法检测到这一点,因此也无法抱怨。不过,加载器/链接器可能(并且经常这样做)。
  • @Walter 编译器和链接器之间没有官方区别。编译器本身并不实现该语言。但是,该标准是故意措辞的,以便在许多情况下可以使用非 C++ 感知的链接器(即平台链接器,最初是为 C 和汇编程序编写的)。特别是旧的链接器仍然不适合,主要是因为实现限制,例如损坏符号的最大长度。
  • @Johannes:回复您的评论;是的,工具链没有在这里检测到问题,这真是太可惜了。然而,这并不容易。在单个二进制文件(库或可执行文件)中,链接器确实可以实现该问题。然而,当使用 DLL 时,符号插入(第一个匹配的符号优先于未来的匹配符号)是一种常用的技术,因此从加载器的角度来看,看到第二个(不同的)定义是完全正常的。我们可以争论该技术的优点,但由于它很常见,没有人愿意破坏它......
  • @inheanyi 这只是一个定义问题。如果按照官方定义,您可以创建一个一次性编译和链接的程序,仍然称其为“编译器”。我认为这就是 ArneVogel 的意思。
猜你喜欢
  • 2021-11-22
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多