Table of Contents

C++20 added a couple of new attributes in the form of [[attrib_name]]. One of them - [[no_unique_address]] - can have surprising effects on the code! In this blog post, you’ll learn how to optimize your classes' layout and make some data members “disappear”. In most cases, it will be just one line of C++20 code.

Let’s go.

A motivating example  

unique_ptr is one of the most useful smart pointers and is also easy to use. It’s very flexible as you can even control the way the deleter works.

I wrote an article on that topic some time ago:

Custom Deleters for C++ Smart Pointers - unique_ptr

To recall the knowledge, let’s have a look at an example. There’s a legacy type LegacyList, and the users are obligated to call ReleaseElements before the list is deleted:

struct LegacyList {
     void ReleaseElements(); // needs to be called before delete

struct LegacyListDeleterFunctor {  
    void operator()(LegacyList* p) {
        delete p;

using unique_legacylist_ptr =  
  std::unique_ptr<LegacyList, LegacyListDeleterFunctor>;

Play with the example @Coliru

As you can see, we can create a unique_ptr that holds the pointer to a LegacyList object and then properly destroys it in the custom deleter.

But there’s another nifty property of unique_ptr related to deleters:

Do you know what the size of the unique_legacylist_ptr is? It holds the pointer and the deleter… so it should be at least 2x pointer size?

But it’s not.

For stateless deleters (to check if the Standard requires it) the size of the unique_ptr is just one pointer! And it’s achieved through Empty Base Class Optimisation.

To understand how to works, we need to open the hood and look at the internals.

Internals of unique_ptr  

For this purpose, let’s go to Github repository for Microsoft STL implementation of unique_ptr:

STL/memory line 2435 · microsoft/STL

and then if we go to line 2558:

You can see the following helper type:

_Compressed_pair<_Dx, pointer> _Mypair;

The implementation stores the pointer and the deleter inside a compressed pair.

Throughout the class code you can notice that unique_ptr uses the _Mypair object to reference the data and the deleter. For example in the destructor:

~unique_ptr() noexcept {
        if (_Mypair._Myval2) {
            _Mypair._Get_first()(_Mypair._Myval2); // call deleter

Ok… but what’s that compressed pair?

The purpose of this class is to hold two types, similarly as std::pair, but when one of those types is empty, then the compressed pair doesn’t use any storage for it.

Wow, looks interesting!

But how does it work?

See below:

Empty Base Class Optimisation  

In C++, there’s a requirement that even a type that has no data members must have a nonzero size.

For example:

struct Empty { };
std::cout << sizeof(Empty); // prints 1

However there’s no requirement about empty base classes, so for example:

struct Empty { };
struct EmptyEmpty : Empty { };
std::cout << sizeof(EmptyEmpty);

It’s also 1… not 1 + 1!

Play with the code @Coliru

That’s why if you know that your class is empty, then you can inherit from that class, and the compiler won’t enlarge your derived class!

Empty classes can represent anything, like a stateless deleter (for example, for unique_ptr), stateless allocator, or a class that implements some interface or policy with only member functions and no state. In fact, in STL, there are many places where this technique is used to save space.

Going back to the compressed pair:

Let’s have a look at the code:

This time we have to go into the xmemory header:

We have two template specialisations:

The first one:

// store a pair of values, deriving from empty first
template <class _Ty1, class _Ty2, bool = is_empty_v<_Ty1> && 
class _Compressed_pair final : private _Ty1 {
    _Ty2 _Myval2;
    // ... the rest of impl

And the second one:

// store a pair of values, not deriving from first
template <class _Ty1, class _Ty2>
class _Compressed_pair<_Ty1, _Ty2, false> final { 
    _Ty1 _Myval1;
    _Ty2 _Myval2;
    // ... the rest of impl

The main trick here is that we need to check if the first type is empty. If it is, then we cannot store any objects as members (as it would take the space, at least 1 byte), but privately derive from it. The inheritance gives us a chance to call member functions of the empty class.

As you can see, the compressed pair is quite simple, as it considers only if the first type is empty. You can also have a look at the compressed pair from the boost library, where the first or the second type can be empty.: Compressed_Pair - Boost 1.73.0

Okay, but this article is in the series about C++20 features…, and clearly, EBO is not a new invention.

That’s why we have to look at the proposal of P0840:

The no_unique_address C++20 attribute  

In C++20, we’ll have an addition, a new attribute that allows us to reduce the need for EBO and rely on the new attribute!

Rather than inheriting and checking if a type is empty or not… we can just write:

template <typename T, typename U>
struct compressed_pair_cpp20 {
    [[no_unique_address]] T _val1;
    [[no_unique_address]] U _val2;

Much simpler!

There’s no need for any template magic here! The compiler can check if the class is empty, and then it’s allowed to use the same address as other non-static data members. It will reuse the space.

The attribute can be applied to non-static data members, which are not bit fields.

For example:

struct Empty { };

compressed_pair_cpp20<int, Empty> p;
std::cout << std::addressof(p._val1) << '\n';
std::cout << std::addressof(p._val2) << '\n';

In both lines, you should see the same address, as _val1, and _val2 will occupy the same position in memory.

Play with the code @Coliru

Other uses  

Thus far, we’ve learned that the new attribute can be used in places like stateless deleter. What are other options?

If we go to the proposal we can see the following code:

template<typename Key, typename Value,
         typename Hash, typename Pred, typename Allocator>
class hash_map {
  [[no_unique_address]] Hash hasher;
  [[no_unique_address]] Pred pred;
  [[no_unique_address]] Allocator alloc;
  Bucket *buckets;
  // ...
  // ...

As you can see, hasher, pred, and alloc have the attribute [[no_unique_address]] applied.

If those non-static data members are empty, they might have the same address as buckets.

It looks like the new attribute is handy for template classes that work with empty data members. This falls into the category of stateless deleters, predicates, allocators, and other “custom” objects that live inside your class.

This article started as preview for Patreon. If you want to get elusive content, early previews, bonus materials and access to Discord server, join
the C++ Stories Premium membership.

Wrap up  

Ok… so we made a little journey inside the STL implementation!

To sum up:

unique_ptr has the optimization where a stateless deleter (a stateless function object or a captur-less lambda) won’t take any space, and the size of this pointer will be just a single pointer type.

Internally, for MSVC (but other vendors have a similar approach) uses compressed pair to store the pointer field and the deleter. The compressed pair uses Empty Base Class Optimisation to compress the space if one of the types is an empty type. EBO uses inheritance, and some template magic is needed to build a proper specialization of the compressed pair class.

(For example GCC libc++ there’s std::tuple used to store the pointer and the deleter, while there’s no requirement on std::tuple to be “compressed” it seems that the GCC implementation uses this approach, see here:

This technique can be significantly simplified in C++20 thanks to the new attribute [[no_unique_address]].

Compiler support  

GCC And Clang support this new attribute since version 9.0, MSVC since VS 2019 16.9(*).

(*): The MSVC seems to recognize this attribute, but not handle it properly due to ABI issues, you can use [[msvc::no_unique_address]] as a workaround. Please have a look at this bug report: for more information. Reported by the following readed at r/cpp comments.

And here’s another important message about the MSVC compiler: MSVC C++20 and the /std:c++20 Switch.