在 C++ 类成员函数上使用 #ifdef 保护是否安全?

use*_*604 18 c++ objective-c conditional-compilation objective-c++ one-definition-rule

假设您有以下 C++ 类的定义:

class A {
// Methods
#ifdef X
// Hidden methods in some translation units
#endif
};
Run Code Online (Sandbox Code Playgroud)

这是否违反了班级的一个定义规则?有哪些相关危害?我怀疑如果使用成员函数指针或虚函数,这很可能会中断。否则使用安全吗?

我在 Objective C++ 的上下文中考虑它。头文件包含在纯 C++ 和 Objective C++ 翻译单元中。我的想法是使用 OBJC 宏保护具有 Objective-C 类型的方法。否则,我必须对标头中的所有 Objective-C 类型使用 void 指针,但这样我就失去了强类型,并且必须在整个代码中添加丑陋的静态强制转换。

Swi*_*Pie 17

是的,如果允许单独的编译单元具有不同的宏定义状态,它可能会导致 ODR 违规的危险XX在每次包含程序的类定义之前,应该在程序(和共享对象)中全局定义(或不​​定义),以满足合规要求。就 C++ 编译器(不是预处理器)而言,它们是两种不同的、不兼容的、不相关的类类型

想象一下在编译单元A.cpp X之前定义class A并且在单元B.cpp X中未定义的情况。如果 B.cpp 中没有使用那些被“删除”的成员,你就不会得到任何编译器错误。两个单元本身都可以被认为是结构良好的。现在,如果B.cpp将包含一个新表达式,它将创建一个不兼容类型的对象,小于A.cpp 中定义的对象。但是class A,当使用在B.cpp 中创建的对象调用时,来自 的任何方法,包括构造函数,都可能通过访问对象存储之外的内存而导致 UB ,因为它们使用更大的定义。

这种愚蠢行为有一个变体,将头文件的副本包含到具有相同文件名和 POD 结构类型的构建树的两个或多个不同文件夹中,其中一个文件夹可以通过#include <filename>. #include "filename"设计为使用替代品的单位。但他们不会。因为在这种情况下头文件查找的顺序是平台定义的,程序员不能完全控制在每个平台上的哪个单元中包含哪个头文件#include "filename"。一旦一个定义被改变,即使只是通过重新排序成员,ODR 也会被破坏。

为了特别安全,这些事情应该只在编译器域中通过使用模板、PIMPL 等来完成。对于语言间通信,应该安排一些中间地带,使用包装器或适配器,C++ 和 ObjectiveC++ 可能具有非兼容的内存布局。 POD 对象。

  • @Peter-ReinstateMonica 除了第一个将 vtable 指针添加到对象的“virtual”成员函数之外,对吗? (3认同)
  • 在人类已知的所有实现中,成员函数的数量不会改变对象的大小。 (2认同)
  • @R2RT 正确。我的意思是非虚拟函数(对于虚拟函数,你的陈述也成立)。 (2认同)

Dav*_*rtz 15

这炸的很厉害。不要这样做。使用 gcc 的示例:

头文件:

// a.h

class Foo
{
public:
    Foo() { ; }

#ifdef A
    virtual void IsCalled();
#endif
    virtual void NotCalled();
};
Run Code Online (Sandbox Code Playgroud)

第一个 C++ 文件:

// a1.cpp

#include <iostream>

#include "a.h"

void Foo::NotCalled()
{
    std::cout << "This function is never called" << std::endl;
}

extern Foo* getFoo();
extern void IsCalled(Foo *f);

int main()
{
   Foo* f = getFoo();
   IsCalled(f);
}
Run Code Online (Sandbox Code Playgroud)

第二个 C++ 文件:

// a2.cpp

#define A
#include "a.h"
#include <iostream>

void Foo::IsCalled(void)
{
    std::cout << "We call this function, but ...?!" << std::endl;
}

void IsCalled(Foo *f)
{
    f->IsCalled();
}

Foo* getFoo()
{
    return new Foo();
}
Run Code Online (Sandbox Code Playgroud)

结果:

这个函数永远不会被调用

哎呀!代码调用虚函数IsCalled,我们分派到,NotCalled因为两个翻译单元在类虚函数表中的哪个条目上存在分歧。

这里出了什么问题?我们违反了 ODR。所以现在两个翻译单元在虚函数表中应该是什么位置上存在分歧。因此,如果我们在一个翻译单元中创建一个类并从另一个翻译单元调用其中的虚函数,我们可能会调用错误的虚函数。哎呀哎呀!

请不要刻意做相关标准规定不允许也不行的事情。你永远无法想到它可能出错的所有可能方式。这种推理在我几十年的编程生涯中造成了许多灾难,我真的希望人们不要刻意制造潜在的灾难。

  • “虚拟成员函数和常规成员函数之间没有根本区别”:这*所以*不是真的。 (8认同)
  • 您和我一样知道,相关的根本区别在于虚拟函数的附加间接,需要具有类型信息的运行时机制来解析调用。纯粹从概念上讲,它们通常无法在编译时解决。相比之下,非虚函数*可以*在编译时解析。这对于性能来说很重要;这就是 Bjarne 有意识地设计决定拥有它们而不是简单地将所有成员功能虚拟化的原因。因此,所有实现都会利用这一点并在编译时解析调用。 (2认同)
  • (ctd。)为此,他们使用古老的 C 机制:成员函数(这里没有告诉您任何新内容)是从链接角度来看的独立函数,它们通过符号名称解析。这种根本区别在这里是相关的,因为对于标准 C 函数调用解析机制来说,声明的顺序是无关紧要的。不过,它通常与虚拟调用的运行时机制相关。既然您知道这一切,我真的不确定您做出该断言或提出该问题的动机是什么;-)。 (2认同)

Bas*_*tch 1

在 C++ 类成员函数上使用 #ifdef 保护是否安全?

在实践中(查看使用 GCC 生成的汇编代码)g++ -O2 -fverbose-asm -S建议做的事情是安全的。理论上不应该。

然而,还有另一种实用的方法(在QtFLTK中使用)。在你的“隐藏”方法中使用一些命名约定(例如,所有这些方法的dontuse名称中都应该包含这样的文档int dontuseme(void)),并编写你的GCC 插件以在编译时警告它们。或者只是在构建过程中使用一些聪明的grep(1)Makefile (例如在您的)

或者,您的 GCC 插件可能会实现新的#pragma-s 或函数属性,并可能警告不要滥用此类函数。

当然,您也可以(巧妙地)使用,最重要的是,在构建过程中生成 C++ 代码(使用像SWIGprivate:这样的生成器)。

所以实际上来说,你的#ifdef守卫可能毫无用处。我不确定它们会使 C++ 代码更具可读性。

如果性能很重要(使用 GCC),请-flto -O2在编译和链接时使用这些标志。

另请参见GNU autoconf - 它使用类似的基于预处理器的方法。

或者使用其他一些预处理器或 C++ 代码生成器GNU m4GPP、您自己用ANTLRGNU bison制作的)生成一些C++ 代码。就像 Qt对其moc.

所以我的观点是你想做的事是没有用的。您未阐明的目标可以通过许多其他方式实现。例如,生成“随机”的 C++ 标识符(或 C 标识符,或 ObjectiveC++ 名称等...) (这是在RefPerSys_5yQcFbU0s中完成的) - 名称的意外冲突是非常不可能的。

您在评论中指出:

否则,我必须对标头中的所有 Objective-C 类型使用 void*,但这样我就会失去强类型

不,您可以生成一些inlineC++ 函数(将使用reinterpret_cast)来再次获得强类型。Qt就是这么做的!FLTKFOXGTKmm也生成 C++ 代码(因为 GUI 代码很容易生成)。

我的想法是使用 OBJC 宏来保护 Objective-C 类型的方法

如果您使用这些宏生成一些 C++ 或 C 或 Objective C 代码,那么这非常有意义。

我怀疑如果使用成员函数指针或虚函数,这很可能会中断。

实际上,如果您生成随机的 C++ 标识符,它不会中断。或者只是如果您在生成的C++ 代码(或生成的 Objective C++ 或生成的 C,...代码)中记录命名约定(如GNU bisonANTLR所做的)

请注意,像GCC这样的编译器目前(2021 年在内部)使用多个 C++ 代码生成器。因此生成 C++ 代码是一种常见的做法。实际上,如果您注意生成“随机”标识符(您可以在构建时将它们存储在某些sqlite数据库中),则名称冲突的风险很小。

还必须在整个代码中添加丑陋的静态转换

如果生成丑陋的代码,这些转换并不重要。

例如,RPCGENSWIG(或Bisoncpp)生成丑陋的 C 和 C++ 代码,但运行良好(也许还有一些专有的ASN.1JSONHTTPSMTPXML相关的内部代码生成器)。

头文件包含在纯 C++ 和 Objective C++ 翻译单元中。

另一种方法是生成两个不同的头文件......

一个用于 C++,另一个用于 Objective C++。SWIG工具可能会带来启发。当然,你的(C 或 C++ 或 Objective C)代码生成器会发出随机的标识符……就像我在Bismon(生成随机的 C 名称,如moduleinit_9oXtCgAbkqv_4y1xhhF5Nhz_BM)和RefPerSys(生成随机的 C++ 名称,如rpsapply_61pgHb5KRq600RLnKD……)中所做的那样;在这两个系统中,意外名称冲突的可能性很小。

当然,原则上,使用#ifdef防护装置并不安全,正如这个答案中所解释的那样。

附言。几年前,我从事GCC MELT工作,它为一些旧版本的GCC编译器生成了数百万行 C++ 代码。今天 - 2021 年 -实际上可以使用asmjitlibgccjit更直接地生成机器代码部分评估是一个很好的概念框架。