我个人有个习惯, 喜欢在摸鱼的时候翻翻自己之前写过的大块代码. 当时在写这种大块代码时是处于一种很激情的状态, 恨不得立刻将脑袋里的想法泼洒到屏幕上实现出来, 所以就需要一种激情冷却下来之后, 一种事后的角度来审视这坨代码, 看看有咩有当时上头时无意留下的一些蠢活. 这不这次摸鱼就发现了如下代码:
struct RawTask {
core::Header* ptr; // always valid!
public:
RawTask(core::Header* p) noexcept: ptr(p) {
assert(p != nullptr);
}
RawTask(RawTask&& o) noexcept:
ptr(o.ptr) {
o.ptr = nullptr;
assert(this->ptr != nullptr);
}
// 展开之后:
// RawTask(const RawTask&) = default.
// RawTask& operator=(const RawTask&) = default;
HOLO_ALLOW_COPY(RawTask);
};
struct Task {
raw::RawTask raw;
public:
~Task() noexcept {
auto& self = *this;
if (self.raw.ptr == nullptr) {
return;
}
if (self.header().state.ref_dec()) {
std::move(self.raw).dealloc();
}
}
}
由于 RawTask::RawTask(RawTask&&)
的存在, 导致 RawTask 不再是 is_trivially_copyable_v
, 这导致了我们可能会丧失一些优化机会, 比如 optional<RawTask>
的移动构造现在也不是 trivially 的了; 而我们代码中可是大量使用 OptPtr<RawTask>
来着; (OptPtr<T>
语义上等同于 optional<T>
, 只不过不会额外使用一个 bool 变量来表示值不存在, 而是零值来表示值不存在, 类似于 rust 中的 Option.)
当时实现 RawTask::RawTask(RawTask&&)
的背景应该是代码中有一些类, 比如上面中的 Task, 其中有个 raw 成员, 并且要求 Task(Task&& o)
移动构造之后, o.raw.ptr = nullptr
. 如 ~Task
实现所示, 这里 core::Header
指向着一坨自带 ref count 的引用计数, 如果 o.raw.ptr
不置为 nullptr, 会导致 double ref_dec 操作, 这会进一步导致 double-free 等问题. 通过在 RawTask 实现这个逻辑避免了这些类再实现自己的 move. 所以我们要移除 RawTask 移动构造, 就必须为这些类实现自己的 move, 比如上面的 Task 对应的移动构造便是:
Task(Task&& o) noexcept:
raw(o.raw.ptr) {
o.raw.ptr = nullptr;
assert(this.raw.ptr != nullptr);
}
为了避免遗漏, 我的做法是将 RawTask(RawTask&&)
标记为 delete, 我本来以为这样所有包含 RawTask 的类, 隐式利用到 RawTask 移动构造的类, 类似上面 Task 的那种类, 都会触发编译报错, 然后我一一修复这些编译报错之后再取消标记 RawTask(RawTask&&)
就万事大吉了. 没想到这一番操作下来之后还是碰到了问题:
struct EcImpl {
RawTask raw;
};
class EcSchedulable: public EcImpl {
public:
EcSchedulable(EcSchedulable&& other) noexcept:
EcImpl(std::move(other)) {
DCHECK(other.raw.ptr == nullptr); // dcheck 失败
DCHECK(this->raw.ptr != nullptr);
}
};
幸亏我这里加了个冗余 dcheck, 自流浪地球饱和式救援学到的饱和式 dcheck 救了我一命== 这可和我一开始的预期不太对啊, 本来预期 RawTask 移动构造标记为 delete 之后, EcImpl 应该也随之不支持移动构造, 即 EcImpl(std::move(other))
应该触发编译报错来着. 简单总结下来, 就是我被下面的 demo 程序困惑了:
#include <utility>
#include <type_traits>
struct S {
void* x;
public:
S() {}
S(S&&) = delete;
S(const S&) = default;
};
struct Z {
S s;
public:
// Z(Z&&) = delete;
};
// static_assert(std::is_trivially_move_constructible_v<S>); // false
static_assert(std::is_trivially_move_constructible<Z>::value, "");
int main() {}
我真的不明白啊, 按照 Move constructors 中规则:
Implicitly-declared move constructor If no user-defined move constructors are provided for a class type (struct, class, or union), and all of the following is true:
- there are no user-declared copy constructors;
- there are no user-declared copy assignment operators;
- there are no user-declared move assignment operators;
- there is no user-declared destructor. then the compiler will declare a move constructor as a non-explicit inline public member of its class with the signature T::T(T&&).
The implicitly-declared or defaulted move constructor for class T is defined as deleted if any of the following is true:
- T has non-static data members that cannot be moved (have deleted, inaccessible, or ambiguous move constructors);
这里 Z 命中了 ‘Implicitly-declared move constructor’, 编译器会加个 Z(Z&&)
声明, 但同时 Z 也命中了 ‘is defined as deleted’ 规则, 即最终编译器会加个 Z(Z&&) = delete
. 但实际上 Z 不光可以 move, 而且还是 trivially move, 这完全不符合下面的描述啊, 毕竟 std::is_trivially_move_constructible_v<S>
可是 false 来着:
Trivial move constructor The move constructor for class T is trivial if all of the following is true:
- the move constructor selected for every non-static class type (or array of class type) member of T is trivial.
最后我把 S::x 的类型替换如下:
struct X {
X() noexcept {}
X(const X&) noexcept {
puts("f");
}
X(X&&) noexcept {
puts("u");
}
X& operator=(X&&) noexcept {
puts("c"); return *this;
}
X& operator=(const X&) noexcept {
puts("k"); return *this;
}
};
在运行时输出了 “f” 时, 瞬间勾起了我很久的记忆, 那是 2016-12-29 时, 我还刚刚入门 C++ primer, 其中提到过一句话:
在回到上面 C++ 标准中关于 Move constructors 的一些规则解释中:
T has non-static data members that cannot be moved
实际上这里 S 虽然 S(S&&) = delete
, 但 S(const S&) = default
, 即 S 是可以 moved! 虽然并不是通过 S 的移动构造来 move, 而是通过 S 的拷贝构造.
the move constructor selected for every non-static class type (or array of class type) member of T is trivial
对于 S 来说, 这里 “move constructor selected” 是 S(const S&)
不是 S(S&&)
!
后记
我大抵是 Rust 用的多了, 完全忘记了 C++ 并没有过真正的 move 语义, 就如同 std::is_trivially_move_constructible
实现一样, C++ 类可以 move 只是意味着存在一个构造函数, 其接受 T&&
类型参数.