BlogFM
BlogFM copied to clipboard
C++ 获取构造函数/析构函数的函数指针
解决方法
先说解决方法, 需求后面再说. 构造函数可以有自定义的, 析构函数只有一个. 而且不能直接获取构造/析构函数的指针.
其中C++允许调用其析构函数, 可参考calling destructor explicitly和Destructors (C++). 所以这样就可能间接获取析构函数的函数地址了(T是模板):
// release the object
void delete_obj(void*p) noexcept { static_cast<T*>(p)->T::~T(); }
那么构造函数呢, 当然也可以直接调用, 不过这不是标准行为:
- MSC: 允许直接调用构造函数
- Clang: 允许直接调用构造函数, 不过需要定义MSC兼容扩展flag[-Wmicrosoft-explicit-constructor-call]
- GCC: 不允许直接调用构造函数
对于不允许的我们可以使用c++ placement new, 不过由于直接调用构造函数这个功能实在太诱惑了(placement new同delete一样会判断this指针, 从而间接增加代码大小, 这个由于是模板, 可不能忽略), 所以大致这么实现:
// create the object
static void create_obj(void* ptr) noexcept {
#ifdef CANNOT_CALL_CONSTRUCTOR_DIRECTLY
// gcc cannot call ctor directly
new(ptr) T();
#else
// msc/clang extended support
static_cast<T*>(ptr)->T::T();
#endif
}
CANNOT_CALL_CONSTRUCTOR_DIRECTLY判断不是msvc或者clang但是定义了ms兼容扩展就可以取消定义.
对于自定义的(包括复制移动)构造函数就可以用c++11带来的完美转发:
// create
template<typename ...Args>
static void create(void* ptr, Args&&... args) noexcept {
#ifdef CANNOT_CALL_CONSTRUCTOR_DIRECTLY
// gcc cannot call ctor directly
new(ptr) T(std::forward<Args>(args)...);
#else
// msc/clang extended support
static_cast<T*>(ptr)->T::T(std::forward<Args>(args)...);
#endif
}
错误处理
这样当然只是意思到了而已, 实际上会报错, 因为T是一个typename而不是一条函数(但是~T就是一条函数了, 神奇的C++或者说微软), 直接调用T::T会提示错误, 直接的解决方法当然是直接全部使用placement new, 但是我实在不想舍弃, 就再用了一层中转...
// func-vtable getter
template<typename T> struct ctor_dtor {
// member
T m;
// ctor
ctor_dtor() noexcept {};
};
这样ctor_dtor<T>::ctor_dtor
就是一条函数了
代码优化
这样的话, 针对一些没有析构函数的对象会生成一堆没用的代码. 这个问题各大编译器厂商都有类似于ICF(等价代码折叠)的技术, 能够在链接时折叠相同的只读常量(包括函数), 不过细节不同. 对于取地址操作的函数, GCC流 会持保留态度, 也就是说这种情况ICF在GCC没用. 这种情况只能祭出神器<type_traits>了:
#include <type_traits>
#include <string>
#include <cstdio>
#include <cstdint>
template<typename T> bool get_td_lambda(T&&) noexcept {
return std::is_trivially_destructible<T>::value;
}
int main(){
std::string aa = "asd";
const auto bool0 = get_td_lambda([]() {});
const auto bool1 = get_td_lambda([aa]() {});
const auto bool2 = get_td_lambda([&aa]() {});
std::printf("%d, %d, %d\n", int(bool0), int(bool1), int(bool2));
}
GCC/MSC 当然都是输出"1, 0, 1", 现在对于空析构函数就可以重定向到同一条了. 不过当然还是没有完美解决, 比如 [aa]() {}
这类的有N个, 虽然都是析构一个std::string但是毕竟lambda不同, gcc还是不能折叠, 如果各位有解决方法的话请回复吧.
应用场景
目前LongUI大致有两种方法:
- 模拟
std::function
- 实现不会模板膨胀的Vector.
第一个就懒得说了. 说说第二个, 滥用模板的话会造成模板膨胀这是自然的, 所以为了轻量化, LongUI实现了一个不会模板膨胀的Vector(但是目前还没用过, 所以暂时被冷藏了) 内部使用的是类似虚表的实现, 所以如果保存的是轻量级对象, 比如说是智能指针这种重点是RAII的, 添加删除的效率将会比较低, 主要体现在:
- 多了一层(类似)虚函数调用, 这个的直接影响比较小
- 上面一条会造成间接影响, 没法内联: 有可能本来内联会被编译器优化成1行的代码变成了10行
- 对象强制保存在栈上, 轻量级对象有可能直接保存在寄存器上, 现在添加删除会强制保存在栈上, 这个也算是第二条中
当然这是轻量级的影响比较大, 重量级比如std::string重点是保存的数据这类, 基本没有太大影响. 当然由于LongUI目前使用的全是POD::Vector, 不用调用构造/析构. 这个类还没用过Orz