C++ 的 std::unique_ptr 有个神奇的特性:如果使用默认的 deleter(即使用 operator delete),或者 non-capturing lambda 作为 deleter,则有

1
sizeof(std::unique<T>) == sizeof(void*);

即整个对象的内存布局和 trivial pointer 一致,没有额外的开销。

这个特性的背后就是 compress-pair;这个设施能够在某个元素是一个 empty class 时避免为其分配内存。

注:这里假设你知道什么是 EBO,以及为什么会有 EBO。

这里自己动手实现一个 compressed pair:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
template<typename Tx, typename Ty, bool = std::is_empty<Tx>::value>
struct CompressedPair : Tx {
Ty second;

template<typename T>
explicit CompressedPair(T&& t)
: Tx(),
second(std::forward<T>(t))
{}

template<typename T, typename U>
CompressedPair(T&& t, U&& u)
: Tx(std::forward<T>(t)),
second(std::forward<U>(u))
{}

Tx& get_first() noexcept
{
return *this;
}

const Tx& get_first() const noexcept
{
return *this;
}
};

template<typename Tx, typename Ty>
struct CompressedPair<Tx, Ty, false> {
Tx first;
Ty second;

template<typename T, typename U>
CompressedPair(T&& t, U&& u)
: first(std::forward<T>(t)),
second(std::forward<U>(u))
{}

Tx& get_first() noexcept
{
return first;
}

const Tx& get_first() const noexcept
{
return first;
}
};

因为 EBO 是实现的核心,而父类的构造顺序先于子类的任何成员,上面将 Tx 作为可被优化的成员。

上面的代码使用 partial specialization 来分离处理 1)可优化 2)不可优化 两种 case:

  • 可优化的,直接使用 EBO;同时因为有继承,所以要访问 first 只需要做 up-cast 即可
  • 不可优化的,两个部分直接按照常规的 pair 实现

PS:is_empty 这里是必需的,并且现代编译器下的实现是直接利用 compiler intrinsic;单纯的 library implementation 会有很大制约。可以参考 https://stackoverflow.com/questions/35531309/how-is-stdis-emptyt-implemented-in-vs2015-or-any-compiler

PPS:上面忽略 Tx 是 final class 的 case,因为这不是我们这个 demo 的重点。

有了 CompressedPair 做基础,我们只要糊上一个前端就可以。

假设我们要实现一个 OutputChannel,可以将内部保存的元素花式输出:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
template<typename T, typename P = default_printer<T>>
class OutputChannel {
public:
template<typename U>
explicit OutputChannel(U&& data)
: cp_(std::forward<U>(data))
{}

template<typename U, typename V>
OutputChannel(U&& data, V&& p)
: cp_(std::forward<V>(p), std::forward<U>(data))
{}

void Log()
{
cp_.get_first()(cp_.second);
}

private:
CompressedPair<P, T> cp_;
};

template<typename T>
struct default_printer {
void operator()(const T& ele)
{
std::cout << ele << std::endl;
}
};

如果我们使用默认的 printer 或者 non-capture lambda,则不会给我们的 instance 增添内存开销:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
OutputChannel<std::string> lc("hello world");
std::cout << "sizeof(lc) = " << sizeof(lc) << std::endl;
static_assert(sizeof(std::string) == sizeof(lc), "error");
lc.Log();

OutputChannel<std::string, void(*)(const std::string&)> lsc("hello world", &SPrint);
std::cout << "sizeof(lsc) = " << sizeof(lsc) << std::endl;
static_assert(sizeof(std::string) == sizeof(lsc) - sizeof(void*), "error");
lsc.Log();

auto up_printer = [](const std::string& s) {
std::string us;
std::transform(s.begin(), s.end(), std::back_inserter(us),
[](char c) { return static_cast<char>(toupper(c)); });
std::cout << us << std::endl;
};

OutputChannel<std::string, decltype(up_printer)> usc("hello world", up_printer);
std::cout << "sizeof(usc) = " << sizeof(usc) << std::endl;
static_assert(sizeof(std::string) == sizeof(usc), "error");
usc.Log();

代码返利参考:https://github.com/kingsamchen/Eureka/tree/master/CompressedPair/compressed_pair