Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

C++ 获取构造函数/析构函数的函数指针 #3

Open
dustpg opened this issue Jul 30, 2018 · 0 comments
Open

C++ 获取构造函数/析构函数的函数指针 #3

dustpg opened this issue Jul 30, 2018 · 0 comments
Assignees
Labels

Comments

@dustpg
Copy link
Owner

dustpg commented Jul 30, 2018

解决方法

先说解决方法, 需求后面再说. 构造函数可以有自定义的, 析构函数只有一个. 而且不能直接获取构造/析构函数的指针.

其中C++允许调用其析构函数, 可参考calling destructor explicitlyDestructors (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大致有两种方法:

  1. 模拟std::function
  2. 实现不会模板膨胀的Vector.

第一个就懒得说了. 说说第二个, 滥用模板的话会造成模板膨胀这是自然的, 所以为了轻量化, LongUI实现了一个不会模板膨胀的Vector(但是目前还没用过, 所以暂时被冷藏了)
内部使用的是类似虚表的实现, 所以如果保存的是轻量级对象, 比如说是智能指针这种重点是RAII的, 添加删除的效率将会比较低, 主要体现在:

  • 多了一层(类似)虚函数调用, 这个的直接影响比较小
  • 上面一条会造成间接影响, 没法内联: 有可能本来内联会被编译器优化成1行的代码变成了10行
  • 对象强制保存在栈上, 轻量级对象有可能直接保存在寄存器上, 现在添加删除会强制保存在栈上, 这个也算是第二条中

当然这是轻量级的影响比较大, 重量级比如std::string重点是保存的数据这类, 基本没有太大影响. 当然由于LongUI目前使用的全是POD::Vector, 不用调用构造/析构. 这个类还没用过Orz

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

1 participant