如何解决在 C++ 类成员函数上使用 #ifdef 保护是否安全? 如果您使用这些宏生成一些 C++ 或 C 或 Objective C 代码,这非常有意义当然,原则上使用 #ifdef 守卫是不安全的,如 XML 中所述
假设您有以下 C++ 类的定义:
class A {
// Methods
#ifdef X
// Hidden methods in some translation units
#endif
};
这是否违反了班级的一个定义规则?有哪些相关危害? 我怀疑如果使用成员函数指针或虚函数,这很可能会中断。不然用起来安全吗?
我正在Objective C++的上下文中考虑它。头文件包含在纯 C++ 和 Objective C++ 翻译单元中。我的想法是使用 OBJC 宏保护具有 Objective-C 类型的方法。否则,我必须对标头中的所有 Objective-C 类型使用 void 指针,但这样我就失去了强类型,并且必须在整个代码中添加丑陋的静态强制转换。
解决方法
是的,如果允许单独的编译单元具有不同的宏定义状态 X
,则可能会导致 ODR 破坏的风险。 X
应该在程序(和共享对象)中全局定义(或不定义),然后每次包含程序的类定义以满足合规要求。就 C++ 编译器(而非预处理器)而言,它们是两种不同、不兼容、不相关的类类型。
想象一下在编译单元 A.cpp class A
之前定义在 X
和单元 B.cpp class A
中的情况没有定义。如果 B.cpp 中没有使用那些被“删除”的成员,你就不会得到任何编译器错误。两个单元本身都可以被认为是结构良好的。现在,如果 B.cpp 包含一个新的表达式,它将创建一个不兼容类型的对象,小于 A.cpp 中定义的对象。但是 #include <filename>
中的任何方法,包括构造函数,在使用 B.cpp 中创建的对象调用时,都可能通过访问对象存储之外的内存而导致 UB,因为它们使用更大的定义。 >
这种愚蠢行为有一个变体,将头文件的副本包含到具有相同文件名和 POD 结构类型的构建树的两个或多个不同文件夹中,其中一个文件夹可以通过 #include "filename"
访问。带有 #include "filename"
的单位旨在使用替代品。但他们不会。因为在这种情况下头文件查找的顺序是平台定义的,程序员不能完全控制在每个平台上哪个单元中包含哪个头文件 {{1}}。一旦更改了一个定义,即使只是重新排序成员,ODR 也被破坏了。
为了特别安全,这些事情应该只在编译器域中通过使用模板、PIMPL 等来完成。对于语言间通信,应该安排一些中间地带,使用包装器或适配器,C++ 和 ObjectiveC++ 可能具有不兼容的内存布局非 POD 对象。
,这太可怕了。不要这样做。使用 gcc 的示例:
头文件:
// a.h
class Foo
{
public:
Foo() { ; }
#ifdef A
virtual void IsCalled();
#endif
virtual void NotCalled();
};
第一个 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);
}
第二个 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();
}
结果:
这个函数永远不会被调用
糟糕!代码调用虚函数 IsCalled
,我们分派给 NotCalled
,因为两个翻译单元在哪个条目在类虚函数表中的位置上存在分歧。
这里出了什么问题?我们违反了 ODR。所以现在两个翻译单元在虚函数表中应该是什么位置上存在分歧。因此,如果我们在一个翻译单元中创建一个类并从另一个翻译单元调用其中的虚函数,我们可能会调用错误的虚函数。哎呀哎呀!
请不要刻意做相关标准规定不允许也不行的事情。你永远无法想到它可能出错的所有可能方式。这种推理在我几十年的编程生涯中造成了许多灾难,我真的希望人们不要刻意制造潜在的灾难。
,在 C++ 类成员函数上使用 #ifdef 保护是否安全?
实践(查看使用 GCC 作为 g++ -O2 -fverbose-asm -S
生成的汇编代码)您建议做的是安全的。理论上不应该。
然而,还有另一种实用方法(在Qt和FLTK中使用)。在您的“隐藏”方法中使用一些命名约定(例如,所有这些方法的名称中都应该包含 dontuse
,例如 int dontuseme(void)
),并编写您的 GCC plugin在编译时警告它们。或者只是在您的构建过程中使用一些巧妙的 grep(1)(例如在您的 Makefile
)
或者,您的 GCC 插件可能会实现新的 #pragma
-s 或函数属性,并且可以警告不要滥用此类函数。
当然,您也可以(巧妙地)使用 private:
,最重要的是,在您的构建过程中生成 C++ 代码(使用像 SWIG 这样的生成器)。
所以实际上,您的 #ifdef
守卫可能没用。而且我不确定它们是否使 C++ 代码更具可读性。
如果性能很重要(使用 GCC),请在编译和链接时使用 -flto -O2
标志。
另见 GNU autoconf - 它使用类似的基于预处理器的方法。
或使用其他一些预处理器或 C++ 代码生成器(GNU m4、GPP,您自己的用 ANTLR 或 GNU bison 制作的)生成一些 C++ 代码。就像 Qt 对 its moc
所做的一样。
所以我的观点是,你想做的事情是没有用的。 您未说明的目标可以通过许多其他方式实现。例如,生成“随机”外观的 C++ 标识符(或 C 标识符,或 ObjectiveC++ 名称等...),如 _5yQcFbU0s
(这是在 RefPerSys 中完成的)- 意外碰撞的名字是非常不可能的。
您在评论中声明:
否则,我必须对标头中的所有 Objective-C 类型使用 void*,但这样我就失去了强类型
不,您可以生成一些 inline
C++ 函数(将使用 reinterpret_cast
)来再次获得强类型。 Qt 这样做! FLTK 或 FOX 或 GTKmm 也会生成 C++ 代码(因为 GUI 代码很容易生成)。
我的想法是使用 OBJC 宏保护具有 Objective-C 类型的方法
如果您使用这些宏生成一些 C++ 或 C 或 Objective C 代码,这非常有意义。
我怀疑如果使用成员函数指针或虚函数,这很可能会中断。
在实践中,如果您生成随机的 C++ 标识符,它不会中断。或者,如果您在 生成 C++ 代码(或生成的 Objective C++,或生成的C,...代码)
请注意,像 GNU bison 这样的编译器如今(2021 年,内部)使用了多个 C++ 代码生成器。所以生成 C++ 代码是一种常见的做法。实际上,如果您负责生成“随机”标识符(您可以在构建时将它们存储在某个 ANTLR 数据库中),则名称冲突的风险很小。
还必须在整个代码中添加丑陋的静态强制转换
如果丑陋代码是生成,这些转换无关紧要。
例如,GCC 和 sqlite - 或 RPCGEN - 生成丑陋的 C 和 C++ 代码,它们运行良好(也许还有一些专有的 SWIG 或 {{3 }} 或 Bisoncpp 或 ASN.1 或 JSON 相关的内部代码生成器)。
头文件包含在纯 C++ 和 Objective C++ 翻译单元中。
另一种方法是生成两个不同的头文件...
一个用于 C++,另一个用于 Objective C++。 HTTP 工具可能会鼓舞人心。当然你的(C或C++或Objective C)代码生成器会发出随机看起来的标识符......就像我在{{ 3}}(生成随机的 C 名称,如 moduleinit_9oXtCgAbkqv_4y1xhhF5Nhz_BM
)和 SMTP(生成随机的 C++ 名称,如 rpsapply_61pgHb5KRq600RLnKD
...);在这两个系统中,意外的名称冲突是非常不可能的。
当然,原则上使用 #ifdef
守卫是不安全的,如 XML 中所述。
附注。几年前,我做过 SWIG 的工作,它为一些旧版本的 Bismon 编译器生成了数百万行 C++ 代码。今天 - 在 2021 年 - 您实际上可以更直接地使用 RefPerSys 或 this answer 生成机器代码。 GCC MELT 是一个很好的概念框架。
版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。