我需要使用一个非常庞大且复杂的头文件类(类似于 boost::multiprecision::cpp_bin_float<76>,下文称之为BHP
),我想通过类似 pimpl 的实现来隐藏它,以减少在相当庞大的项目中的编译时间(将 Boost 类替换为std::complex<double>
可以将编译时间缩短约50%)。
然而,我想避免动态内存分配。因此,像这样的东西似乎很自然(暂时忽略可以使用 aligned_storage
或 alignas
避免的对齐问题):
struct Hidden {
char data[sz];
Hidden& punned(Hidden const& other);
};
Hidden::punned
可以在单个翻译单位中定义,将data
转换为BHP*
,对其进行操作,并且不会污染所有其他翻译单位的170k LOC头文件。可能的实现方式如下:
Hidden& Hidden::punned(Hidden const& other) {
*(BHP*)(data) += *(BHP*)(other.data);
return *this;
}
当然,这是未定义的行为,因为我们通过
char
类型的指针访问BHP
类型的对象,违反了严格别名规则。正确的做法是:Hidden& Hidden::proper(Hidden const& other) {
BHP tmp; std::memcpy(&tmp, data, sz);
BHP tmp2; std::memcpy(&tmp2, other.data, sz);
tmp += tmp2;
std::memcpy(data, &tmp, sz);
return *this;
}
现在看起来很明显这些
memcpy
调用可以被优化掉。不幸的是,这不是情况,它们仍然存在并且使proper()
比punned()
大得多。
我想知道正确的方法是a)直接将数据存储在Hidden
对象中和b)避免不必要的复制以重新解释它,并且c)避免违反严格的对齐规则以及d)不需要携带指向存储区域的额外指针。
这里有一个godbolt链接;请注意,我测试过的所有编译器(GCC 4.9 - trunk,Clang 3.9、4.0和5.0以及Intel 18)都没有“优化掉” memcpy。一些版本的GCC(例如5.3)也直接抱怨违反了严格别名规则,但并非所有版本都是如此。我还插入了一个Direct
类,它了解BHP
,因此可以直接调用它,但我想避免这种情况。
最小工作示例:
#include <cstring>
constexpr std::size_t sz = 64;
struct Base {
char foo[sz];
Base& operator+=(Base const& other) { foo[0] += other.foo[0]; return *this; }
};
typedef Base BHP;
// or:
//#include <boost/multiprecision/cpp_bin_float.hpp>
//typedef boost::multiprecision::number<boost::multiprecision::cpp_bin_float<76> > BHP;
struct Hidden {
char data[sz];
Hidden& proper(Hidden const& other);
Hidden& punned(Hidden const& other);
};
Hidden& Hidden::proper(Hidden const& other) {
BHP tmp; std::memcpy(&tmp, data, sz);
BHP tmp2; std::memcpy(&tmp2, other.data, sz);
tmp += tmp2;
std::memcpy(data, &tmp, sz);
return *this;
}
Hidden& Hidden::punned(Hidden const& other) {
*(BHP*)(data) += *(BHP*)(other.data);
return *this;
}
struct Direct {
BHP member;
Direct& direct(Direct const& other);
};
Direct& Direct::direct(Direct const& other) {
member += other.member;
return *this;
}
struct Pointer {
char storage[sz];
BHP* data;
Pointer& also_ok(Pointer const& other);
};
Pointer& Pointer::also_ok(Pointer const& other) {
*data += *other.data;
return *this;
}
char*
并通过后者重新解释是免除别名的。它必须这样做,才能实现memcpy()
(没有编译器技巧)和使用它进行“位转换”的过程。但是,反之则不成立。 - underscore_dchar*
强制转换并使用它来读取对象的表示形式,即将对象重新解释为字节。这是允许的。将一些仅仅是一组字节的东西强制转换为其他类型,而它不是以该类型构造的,则不行。换句话说,从更改类型的强制转换获得的指针只有在那里真正存在该类型的对象(或兼容的对象)时才能被解除引用;我们不能只创建一个char
数组,填充数据并告诉编译器:“这里:有一个SomeClass
”。但是我们可以说:“显示组成此SomeClass
的char
”。 - underscore_d