一种用C++手动实现胖指针的笔记

世界好乱, 就是这样… 虽然, 不论是为了粉饰太平还是怎么样, 大概写点学术化的内容也是好的… (超小声(
不发在邮件列表或者senioria自己的blog里大概一部分是因为懒, 一部分是因为邮件列表里更强的姐姐更多, 怕被嫌弃之类的吧… (超小声(
虽然大概转头就发到邮件列表里了\ x (超小声(

这是senioria在做自己的dotfiles installer时搓出来的一点事情, 其实挺平凡的, 大概稍微水出来一点… (超小声(

平凡的工作: 胖指针

接口

首先, 我们考虑一下我们在Rust里常用的操作(为了简化起见, 忽略了错误处理, 毕竟在c++版本里这也不是很重要的事情 其实只是因为懒\ x):

/// 一个安装器的接口
trait Installer {
    fn is_installed(&mut self) -> bool;
    fn install(&mut self);
}

/// 上面那个接口的使用例
fn do_install(inst: &mut dyn Installer) {
    if !inst.is_installed() {
        inst.install();
    }
}

这是个很平凡的动态派发的例子, 当然C++里面向对象也能很简洁地实现这样的功能, 不过这里的主题是Rust风格的traits (modern C++的concepts), 所以我们的接口如下:

template<class T>
concept Installer = requires(const T a) {
    { a.is_installed() } -> std::convertible_to<bool>;
} && requires(T a) {
    a.install();
};

这里, move_constructible 的约束在Rust里是比较平凡的 (senioria的印象里似乎只有 !Unpin 的类型才不能move, 而Rust里的move本身是平凡的), 可惜C++并未如此设计, 感觉原因可能是兼容性: 一些copy可能有业务功能从而非平凡, 如果改成move可能破坏这部分代码, 虽然这样的代码毋庸置疑坏坏, 但毕竟标准和语义都允许了…

虚表, 和胖指针实现

光有个接口本身只能做静态派发, 因为C++并没有给concepts做RTTI, 所以这部分的工作需要我们自己学着Rust/C++里的虚表做:

struct InstallerVtable_t
{
    bool (*is_installed)(const void *self);
    void (*install)(void *self);
};
template<Installer T>
InstallerVtable_t InstallerVTable {
    [](const void *self) { return reinterpret_cast<const T *>(self)->is_installed(); },
    [](void *self) { reinterpret_cast<T *>(self)->install(); },
};

这段代码大概非常简单直接: 虚表声明部分擦除掉this的类型, 虚表的实现部分再把this强制转换回来. 因为下面的使用例子保证了这里指针的类型和指针指向对象的类型相同, 所以就算那真的是个虚函数也可以保证会调用到对象对应的函数, 再通过我们这里手动模拟的机制来做动态派发.

以及擦除引用的类型, 转化为胖指针的代码:


struct DynInstaller
{
private:
    InstallerVtable_t *vt;
    void *data;

public:
    template<Installer T>
    DynInstaller(T &val) : vt(&InstallerVTable<T>), data(&val)
    {
    }
    DynInstaller(const DynInstaller &) = default;
    DynInstaller &operator=(const DynInstaller &) = default;
    DynInstaller(DynInstaller &&o) = default;
    DynInstaller &operator=(DynInstaller &&) = default;
    bool is_installed() const { return vt->is_installed(data); }
    void install() { vt->install(data); }
};

这部分代码也很平凡, 转发一遍实现, 抄一遍复制和移动来堵编译器的警告. 这个类型甚至连内存布局都和Rust的&dyn Trait一模一样.

至于cv限定方面的问题, 虽然C++的const是浅的, 但这里的const也能阻止会改变data指向的对象的函数调用, 所以也不会造成影响. 而对于能否调用正确的函数… 对每个类型,


那么… 平凡的部分的工作似乎就这些了… ?

稍微好玩的部分: 有所有权的多态对象

接口

还是上面那个Rust的接口, 我们抄一遍:

/// 一个安装器的接口
trait Installer {
    fn is_installed(&mut self) -> bool;
    fn install(&mut self);
}

它还有一个好玩的用法:

/// ~~装在套子里的~~ 当前函数所有的安装器
fn do_install_owned(mut inst: Box<dyn Installer>) {
    if !inst.is_installed() {
        inst.install();
    }
}

是的, 既然inst为当前函数所有, 那么当前函数也得负责drop它. 在Rust里, 这件事是由编译器完成的, senioria不了解, 所以也不好说rustc在这里做了什么工作. 不过可以知道的是, drop本来也就在这个动态对象的虚表里, 有所有权, 要负责drop的时候, 直接按照drop规则调用就好了; 我们上面的那个C++实现没有所有权, 所以也不用往虚表里放析构函数, 这次其实放进去就好:

template<class T>
concept Installer = requires(const T a) {
    { a.is_installed() } -> std::convertible_to<bool>;
} && requires(T a) { a.install(); }
&& !std::is_reference_v<T> && std::move_constructible<T> &&std::is_destructible_v<T>;

嗯… 是的, 这个谓词不能换成<concepts>里的std::is_destructible, 因为那个同时还要求析构函数不抛出异常… 虽然析构函数确实也该要求不抛出异常… 但这里我们还是尽量做得通用一点. 另外, 注意到std::is_destructible_v还会认为数组类型和引用类型可析构, 这里我们可以忽略数组类型, 因为目前为止C++还不允许数组类型满足上面的两条requires子句; 不幸的是, 因为不是对象, C++的引用有诸多特殊之处, 其中最致命的一点是, 到引用的指针不存在… 这样的话, 至少senioria不知道有什么办法构造出虚函数的参数了. 何况, 就算可以绕过, "拥有"引用的语义本身也是存疑的… 所以加了个不能是引用的约束.

话说回来, 在Rust里, 就senioria的印象而言, 对象永远是可以析构的, 区别只在于其drop是否平凡 (虽然, 就算真的平凡, 在该考虑的情况下总是认为有个不平凡的drop其实总是没问题的); 而C++允许删除任何函数… 一个可能的用法是直接禁止这一类型的任何对象存在 — 就算所有构造函数都已删除, 在类型拥有标准布局的情况下, 通过强制转换构造对象也是完全符合标准的, 更别提senioria印象里所有主流编译器都不会那么无聊地把有相同前缀的类型的这一相同前缀弄出不同的内存布局, 所以就算没有标准布局, 甚至虚函数什么的也都不一样, 实践上其实大概还是可以期待一个强制转换总是可以有着期待中的行为, 虽然senioria也构造不出来这么做的好例子 — 而删除析构函数则最多只允许对象在某块内存区域中短暂地在概念上存在, 以强制转换一个到这片内存区域的引用到一个对这一类型的引用的形式.

这时候就会觉得Rust的规定是文明了… — 除了明说平台相关的东西之外, 所有类型的布局都是良定义的. 虽然, 直球翻译成C++, 具体实现中的行为大抵也一样 — 只要编译器不依赖这一UB做优化.

那么, 做了这么多让事情变得平凡的约束之后, 我们可以简单地写出一个和上面那个大同小异的虚表:

struct InstallerVtable_t
{
    bool (*is_installed)(const void *self);
    void (*install)(void *self);
    void (*destructor)(void *self);
};
template<Installer T>
InstallerVtable_t InstallerVTable {
    [](const void *self) { return reinterpret_cast<const T *>(self)->is_installed(); },
    [](void *self) { reinterpret_cast<T *>(self)->install(); },
    [](void *self) { reinterpret_cast<T *>(self)->~T(); },
};

虽然有个东西叫预期的析构函数, 但好消息是到了这一步 (反正虚表本身不在某个类的命名空间里), T的析构函数早已确定, 所以直接调用不会有问题. 而对于虚析构函数, 我们在后面可以看到, 就算强行通过强制转换构造了指向派生类对象的基类引用, 后面的构造过程也会在对象切片里抹去这一信息.

实现

虽然似乎东西有点多, 但还是直接贴代码:

struct BoxInstaller
{
private:
    InstallerVtable_t *vt;
    void *data;

public:
    template<Installer T>
    BoxInstaller(T &&val)
        : vt(&InstallerVTable<T>), data(reinterpret_cast<char *>(std::aligned_alloc(alignof(T), sizeof(T))))
    {
        new (data) T(std::move(val));
    }
    ~BoxInstaller()
    {
        if (data)
            vt->destructor(data);
        std::free(data);
    }
    BoxInstaller(const BoxInstaller &) = delete;
    BoxInstaller &operator=(const BoxInstaller &) = delete;
    BoxInstaller(BoxInstaller &&o) : vt(o.vt), data(o.data) { o.data = nullptr; }
    BoxInstaller &operator=(BoxInstaller &&o)
    {
        if (this == &o)
            return *this;
        this->~BoxInstaller();
        new (this) BoxInstaller(std::move(o));
        return *this;
    }
    bool is_installed() const { return vt->is_installed(data); }
    void install() { vt->install(data); }
};

那么可以看到, 这个对象的布局还是那副Rust动态对象的样子.

碎碎念: 就算对对象布局的限制在C++23里收紧到严格按照声明顺序了, C++的标准布局要求仍然要求对象的各个成员具有相同的访问控制… senioria不知道这一要求的原因何在 — 声明一个与某个第三方类型有相同成员布局的类型, 并且打算使用这一事实本身就已经在严重破坏封装了, 再从标准层面上阴阴地来一句这样的重新解释不合理, 又有什么意义呢? 何况, 实现中似乎很大程度上也仍然要, 并且会, 按照标准布局的要求来安排这样的类型的内存布局的. 进一步的, C++本身的哲学本就是不加限制. (虽然, 不论如何, 这个对象仍然是标准布局的)

在碎碎念之外, 这个实现比起上一个要不平凡了许多. 先抛开复制不论, 掌握对象所有权本身也意味着要对对象的生命周期负责, 在这个意义上, 移动和析构的实现其实也蛮平凡的, 构造本身的逻辑也是平凡且显然的. 不是那么明显的地方在于那个std::aligned_alloc, 虽然这一操作本身的意图大概明显, 但其实"需要对齐"这个事实似乎并不是那么显然… 大概是senioria傻傻了吧…
那么说到复制, 之所以删除了复制… 其实主要原因是懒 x 不过就这里而言, T本身可以是或者不是可复制的 (这里是不是可平凡复制倒是没那么重要 — 只要可以, 编译器都会可靠地生成我们需要的代码的), 那么复制本身其实算是另一个trait了 — 正如Rust里的Copy/Clone. 所以… 虽然它是特殊的, 但senioria大概还是感觉不论实现复制与否, 这里的讨论也足够一般了. 而且… Rust也是这么干的 (!) (光速逃(((

收尾的一些话

那么, senioria做所有这些测试的代码如下:

Code
// vim: fdm=marker
#include <algorithm>
#include <concepts>
#include <cstdio>
#include <functional>
#include <string>
#include <type_traits>
#include <vector>
using std::string_literals::operator""s;

// {{{ Test utils
struct TestResult
{
    std::string expected, result;
};
struct TestDriver
{
    struct TestName
    {
        const char *name;
        friend TestDriver operator+(TestName &&n, auto fn)
        {
            TestDriver::tests.push_back({ n.name, fn });
            return TestDriver {};
        }
    };
    inline static std::vector<std::pair<const char *, TestResult (*)(TestDriver *)>> tests;
    inline static bool fliped = false;
    ~TestDriver()
    {
        if (!fliped)
        {
            std::reverse(tests.begin(), tests.end());
            fliped = true;
        }
        auto [name, fn] = tests.back();
        tests.pop_back();
        printf("test %s: ", name);
        if (auto [expected, result] = fn(nullptr); expected == result)
            puts("passed");
        else
            printf("failed, expected '%s', got '%s'\n", expected.c_str(), result.c_str());
    }
};
#define add_test_impl(name, ln)                                                                                        \
    TestDriver test_adder_##ln = TestDriver::TestName { name } + [](TestDriver *) -> TestResult
#define add_test_wrap(name, ln) add_test_impl(name, ln)
#define add_test(name) add_test_wrap(name, __LINE__)
// }}} End test utils

namespace Installers  // {{{ Installer types for test
{
template<int N>
struct Dummy
{
    std::string *out;
    bool inst = false;
    bool is_installed() const
    {
        *out += "check(" + std::to_string(N) + " -> " + (inst ? "installed" : "not installed") + "); ";
        return inst;
    }
    void install()
    {
        *out += "install(" + std::to_string(N) + "); ";
        inst = true;
    }
    Dummy(std::string &o) : out(&o) {}
    Dummy(const Dummy &) = delete;
    Dummy &operator=(const Dummy &) = delete;
    Dummy(Dummy &&o) : out(o.out), inst(o.inst) { o.out = nullptr; }
    Dummy &operator=(Dummy &&o)
    {
        if (this == &o)
            return *this;
        this->~Dummy();
        new (this) Dummy(std::move(o));
        return *this;
    }
    ~Dummy()
    {
        if (out)
            *out += "drop(" + std::to_string(N) + "); ";
    }
};

struct Base
{
    std::string *out;
    bool inst = false;
    Base(std::string &o) : out(&o) {}
    virtual bool is_installed() const
    {
        *out += "check(base -> "s + (inst ? "installed" : "not installed") + "); ";
        return inst;
    }
    virtual void install()
    {
        *out += "install(base); ";
        inst = true;
    }
    Base(const Base &) = delete;
    Base &operator=(const Base &) = delete;
    Base(Base &&o) : out(o.out), inst(o.inst) { o.out = nullptr; }
    Base &operator=(Base &&o)
    {
        if (this == &o)
            return *this;
        this->~Base();
        new (this) Base(std::move(o));
        return *this;
    }
    virtual ~Base()
    {
        if (out)
            *out += "drop(base); ";
    }
};

struct Derived : public Base
{
    Derived(std::string &o) : Base(o) {}
    Derived(Derived &&o) : Base(std::move(o)) {}
    Derived &operator=(Derived &&o)
    {
        if (this == &o)
            return *this;
        this->~Derived();
        new (this) Derived(std::move(o));
        return *this;
    }
    virtual bool is_installed() const override
    {
        *out += "check(derived -> "s + (inst ? "installed" : "not installed") + "); ";
        return inst;
    }
    virtual void install() override
    {
        *out += "install(derived); ";
        inst = true;
    }
    virtual ~Derived()
    {
        if (out)
            *out += "drop(derived); ";
    }
};
}  // }}} End namespace Installers

namespace Sec1_Trivial  // {{{
{
template<class T>
concept Installer = requires(const T a) {
    { a.is_installed() } -> std::convertible_to<bool>;
} && requires(T a) { a.install(); };

struct InstallerVtable_t
{
    bool (*is_installed)(const void *self);
    void (*install)(void *self);
};
template<Installer T>
InstallerVtable_t InstallerVTable {
    [](const void *self) { return reinterpret_cast<const T *>(self)->is_installed(); },
    [](void *self) { reinterpret_cast<T *>(self)->install(); },
};

struct DynInstaller
{
private:
    InstallerVtable_t *vt;
    void *data;

public:
    template<Installer T>
    DynInstaller(T &val) : vt(&InstallerVTable<T>), data(&val)
    {
    }
    DynInstaller(const DynInstaller &) = default;
    DynInstaller &operator=(const DynInstaller &) = default;
    DynInstaller(DynInstaller &&o) = default;
    DynInstaller &operator=(DynInstaller &&) = default;
    bool is_installed() const { return vt->is_installed(data); }
    void install() { vt->install(data); }
};

// {{{ tests
add_test("sec1 basic dummy")
{
    std::string res;
    auto try_install = [&](DynInstaller inst) {
        if (!inst.is_installed())
            inst.install();
        else
            res += "try_install -> installed; ";
    };
    Installers::Dummy<1> a { res };
    Installers::Dummy<2> b { res };
    try_install(a);
    try_install(a);
    b.install();
    try_install(b);
    return { "check(1 -> not installed); install(1); check(1 -> installed); try_install -> installed; "
             "install(2); check(2 -> installed); try_install -> installed; ",
             res };
};

add_test("sec1 derived")
{
    std::string res;
    auto try_install = [&](DynInstaller inst) {
        if (!inst.is_installed())
            inst.install();
        else
            res += "try_install -> installed; ";
    };
    Installers::Base a(res);
    Installers::Derived b(res);
    try_install(a);
    try_install(b);
    return { "check(base -> not installed); install(base); check(derived -> not installed); install(derived); ", res };
};
// }}} End tests
}  // }}} End namespace Sec1_Trivial

namespace Sec2_Owned  // {{{
{
template<class T>
concept Installer = requires(const T a) {
    { a.is_installed() } -> std::convertible_to<bool>;
} && requires(T a) { a.install(); }
#if defined(__GNUC__) && !defined(__clang__)  // Clang would refuse the constraints
&& !std::is_reference_v<T> && std::move_constructible<T> && std::is_destructible_v<T>
#endif
;

struct InstallerVtable_t
{
    bool (*is_installed)(const void *self);
    void (*install)(void *self);
    void (*destructor)(void *self);
};
template<Installer T>
InstallerVtable_t InstallerVTable {
    [](const void *self) { return reinterpret_cast<const T *>(self)->is_installed(); },
    [](void *self) { reinterpret_cast<T *>(self)->install(); },
    [](void *self) { reinterpret_cast<T *>(self)->~T(); },
};

struct BoxInstaller
{
private:
    InstallerVtable_t *vt;
    void *data;

public:
    template<Installer T>
    BoxInstaller(T &&val)
        : vt(&InstallerVTable<T>), data(reinterpret_cast<char *>(std::aligned_alloc(alignof(T), sizeof(T))))
    {
        new (data) T(std::move(val));
    }
    ~BoxInstaller()
    {
        if (data)
            vt->destructor(data);
        std::free(data);
    }
    BoxInstaller(const BoxInstaller &) = delete;
    BoxInstaller &operator=(const BoxInstaller &) = delete;
    BoxInstaller(BoxInstaller &&o) : vt(o.vt), data(o.data) { o.data = nullptr; }
    BoxInstaller &operator=(BoxInstaller &&o)
    {
        if (this == &o)
            return *this;
        this->~BoxInstaller();
        new (this) BoxInstaller(std::move(o));
        return *this;
    }
    bool is_installed() const { return vt->is_installed(data); }
    void install() { vt->install(data); }
};

// {{{2 tests
add_test("sec2 basic dummy")
{
    std::string res;
    auto try_install = [&](BoxInstaller inst) {
        if (!inst.is_installed())
            inst.install();
        else
            res += "try_install -> installed; ";
        if (!inst.is_installed())
            inst.install();
        else
            res += "try_install -> installed; ";
    };
    Installers::Dummy<1> a(res);
    try_install(std::move(a));
    Installers::Dummy<2> b(res);
    try_install(std::move(b));
    return { "check(1 -> not installed); install(1); check(1 -> installed); try_install -> installed; drop(1); "
             "check(2 -> not installed); install(2); check(2 -> installed); try_install -> installed; drop(2); ",
             res };
};

add_test("sec2 basic deriving")
{
    std::string res;
    auto try_install = [&](BoxInstaller inst) {
        if (!inst.is_installed())
            inst.install();
        else
            res += "try_install -> installed; ";
    };
    Installers::Base a(res);
    Installers::Derived d(res);
    try_install(std::move(a));
    try_install(std::move(d));
    return { "check(base -> not installed); install(base); drop(base); "
             "check(derived -> not installed); install(derived); drop(derived); drop(base); ",
             res };
};

add_test("sec2 force deriving")
{
    std::string res;
    auto try_install = [&](BoxInstaller inst) {
        if (!inst.is_installed())
            inst.install();
        else
            res += "try_install -> installed; ";
    };
    Installers::Derived d(res);
    try_install(static_cast<Installers::Base &&>(d));
    return { "check(base -> not installed); install(base); drop(base); ", res };
};
// }}} End tests
}  // }}} End namespace Sec2_Owned

int main() {}

在这份代码里, senioria自己轮了个测试框架, 然后随便把一些东西扔进去测了测, 看样子像是没问题了的样子.

总之… 随手轮了这么篇东西出来, 目的什么的大概也没什么… (超小声(

13 Likes

\senioria/

4 Likes