62

i have read the new c++20 feature no_unique_address several times and i hope if some one can explain and illustrate with an example better than this example below taken from c++ reference.

Explanation Applies to the name being declared in the declaration of a non-static data member that's not a bit field.

Indicates that this data member need not have an address distinct from all other non-static data members of its class. This means that if the member has an empty type (e.g. stateless Allocator), the compiler may optimise it to occupy no space, just like if it were an empty base. If the member is not empty, any tail padding in it may be also reused to store other data members.

#include <iostream>
 
struct Empty {}; // empty class
 
struct X {
    int i;
    Empty e;
};
 
struct Y {
    int i;
    [[no_unique_address]] Empty e;
};
 
struct Z {
    char c;
    [[no_unique_address]] Empty e1, e2;
};
 
struct W {
    char c[2];
    [[no_unique_address]] Empty e1, e2;
};
 
int main()
{
    // e1 and e2 cannot share the same address because they have the
    // same type, even though they are marked with [[no_unique_address]]. 
    // However, either may share address with c.
    static_assert(sizeof(Z) >= 2);
 
    // e1 and e2 cannot have the same address, but one of them can share with
    // c[0] and the other with c[1]
    std::cout << "sizeof(W) == 2 is " << (sizeof(W) == 2) << '\n';
}
  1. can some one explain to me what is the purpose behind this feature and when should i use it?
  2. e1 and e2 cannot have the same address, but one of them can share with c[0] and the other with c[1] can some one explain? why do we have such kind of relation ?
NathanOliver
  • 171,901
  • 28
  • 288
  • 402
Adam
  • 2,820
  • 1
  • 13
  • 33
  • 1
    Here's one person who'd be happy to use it https://stackoverflow.com/questions/57460260/trying-to-get-rid-of-empty-data-fields-a-kind-of-empty-base-optimization Then there's the age old uses of EBO https://stackoverflow.com/questions/4325144/when-do-programmers-use-empty-base-optimization-ebo - except we can use composition instead of abusing inheritance – StoryTeller - Unslander Monica Jul 07 '20 at 22:27
  • Neither gcc (trunk) nor clang (trunk) on godbolt make `sizeof(W) == 2` (`struct A` in linked example), however they both do if the declarations with `[[no_unique_address]]` come before the other declarations. [Example](https://godbolt.org/z/WTdGxP) – pizzapants184 Jul 09 '20 at 06:35

3 Answers3

87

The purpose behind the feature is exactly as stated in your quote: "the compiler may optimise it to occupy no space". This requires two things:

  1. An object which is empty.

  2. An object that wants to have an non-static data member of a type which may be empty.

The first one is pretty simple, and the quote you used even spells it out an important application. Objects of type std::allocator do not actually store anything. It is merely a class-based interface into the global ::new and ::delete memory allocators. Allocators that don't store data of any kind (typically by using a global resource) are commonly called "stateless allocators".

Allocator-aware containers are required to store the value of an allocator that the user provides (which defaults to a default-constructed allocator of that type). That means the container must have a subobject of that type, which is initialized by the allocator value the user provides. And that subobject takes up space... in theory.

Consider std::vector. The common implementation of this type is to use 3 pointers: one for the beginning of the array, one for the end of the useful part of the array, and one for the end of the allocated block for the array. In a 64-bit compilation, these 3 pointers require 24 bytes of storage.

A stateless allocator doesn't actually have any data to store. But in C++, every object has a size of at least 1. So if vector stored an allocator as a member, every vector<T, Alloc> would have to take up at least 32 bytes, even if the allocator stores nothing.

The common workaround to this is to derive vector<T, Alloc> from Alloc itself. The reason being that base class subobject are not required to have a size of 1. If a base class has no members and has no non-empty base classes, then the compiler is permitted to optimize the size of the base class within the derived class to not actually take up space. This is called the "empty base optimization" (and it's required for standard layout types).

So if you provide a stateless allocator, a vector<T, Alloc> implementation that inherits from this allocator type is still just 24 bytes in size.

But there's a problem: you have to inherit from the allocator. And that's really annoying. And dangerous. First, the allocator could be final, which is in fact allowed by the standard. Second, the allocator could have members that interfere with the vector's members. Third, it's an idiom that people have to learn, which makes it folk wisdom among C++ programmers, rather than an obvious tool for any of them to use.

So while inheritance is a solution, it's not a very good one.

This is what [[no_unique_address]] is for. It would allow a container to store the allocator as a member subobject rather than as a base class. If the allocator is empty, then [[no_unique_address]] will allow the compiler to make it take up no space within the class's definition. So such a vector could still be 24 bytes in size.


e1 and e2 cannot have the same address, but one of them can share with c[0] and the other with c1 can some one explain? why do we have such kind of relation ?

C++ has a fundamental rule that its object layout must follow. I call it the "unique identity rule".

For any two objects, at least one of the following must be true:

  1. They must have different types.

  2. They must have different addresses in memory.

  3. They must actually be the same object.

e1 and e2 are not the same object, so #3 is violated. They also share the same type, so #1 is violated. Therefore, they must follow #2: they must not have the same address. In this case, since they are subobjects of the same type, this means that the compiler-defined object layout of this type cannot give them the same offset within the object.

e1 and c[0] are distinct objects, so again #3 fails. But they satisfy #1, since they have different types. Therefore (subject to the rules of [[no_unique_address]]) the compiler could assign them to the same offset within the object. The same goes for e2 and c[1].

If the compiler wants to assign two different members of a class to the same offset within the containing object, then they must be of different types (note that this is recursive through all of each of their subobjects). Therefore, if they have the same type, they must have different addresses.

Nicol Bolas
  • 449,505
  • 63
  • 781
  • 982
  • This is the first time I hear about that "unique identity rule". I know it probably wouldn't be of much use to derive from the same class multiple times (probably neither legal, I'm not sure), but does that rule also apply to base classes? – DeltA Oct 19 '22 at 04:03
  • @DeltA: Objects are objects, whether they are members, base classes, or complete objects. All objects are subject to the same rule (at least, as far as the implementation's layout of objects is concerned). – Nicol Bolas Oct 19 '22 at 05:45
24

In order to understand [[no_unique_address]], let's take a look at unique_ptr. It has the following signature:

template<class T, class Deleter = std::default_delete<T>>
class unique_ptr;

In this declaration, Deleter represents a type which provides the operation used to delete a pointer.

We can implement unique_ptr like this:

template<class T, class Deleter>
class unique_ptr {
    T* pointer = nullptr;
    Deleter deleter;

   public:
    // Stuff

    // ...

    // Destructor:
    ~unique_ptr() {
        // deleter must overload operator() so we can call it like a function
        // deleter can also be a lambda
        deleter(pointer);
    }
};

So what's wrong with this implementation? We want unique_ptr to be as light-weight as possible. Ideally, it should be the exact same size as a regular pointer. But because we have the Deleter member, unqiue_ptr will end up being at least 16 bytes: 8 for the pointer, and then 8 additional ones to store the Deleter, even if Deleter is empty.

[[no_unique_address]] solves this issue:

template<class T, class Deleter>
class unique_ptr {
    T* pointer = nullptr;
    // Now, if Deleter is empty it won't take up any space in the class
    [[no_unique_address]] Deleter deleter;
   public:
    // STuff...
Alecto Irene Perez
  • 10,321
  • 23
  • 46
  • 2
    +1, but this is not really an issue in practice because most, if not all, `unique_ptr` implementations avoid the problem by storing the pointer and deleter in `compressed_pair` or similar type that uses empty base optimization. Of course, as Nicol's answer says, that's not foolproof because you may have a `final` deleter type etc. – Praetorian Jul 07 '20 at 22:41
  • 2
    @Praetorian: "*this is not really an issue in practice*" Why not? Because normal C++ programmers aren't allowed to write `compressed_pair` on their own? The fact that `boost::compressed_pair` exists *at all* is proof enough that this is a thing people have a real, practical need for. Just like `boost::noncopyable` is proof that we needed a way to make types that were not copyable. – Nicol Bolas Jul 07 '20 at 22:44
  • 1
    @Nicol I think you misunderstood what I was trying to say. I meant `unique_ptr` stdlib implementations don't exceed the size of a pointer, assuming the deleter is stateless. The answer seems to imply the `unique_ptr` size will always be greater than that prior to the existence of `[[no_unique_address]]`. – Praetorian Jul 07 '20 at 23:02
  • 2
    @Praetorian If an issue can be worked around by increasing complexity, it's still an issue. – Dmitry Grigoryev Jul 09 '20 at 07:30
13

While the other answers explained it pretty well already, let me explain it from a slightly different perspective:

The root of the problem is that C++ does not allow for zero sized objects (i.e. we always have sizeof(obj) > 0).

This is essentially a consequence of very fundamental definitions in the C++ standard: The unique identity rule (as Nicol Bolas explained) but also from the definition of the "object" as a non-empty sequence of bytes.

However this leads to unpleasant issues when writing generic code. This is somewhat expected because here a corner-case (-> empty type) receives a special treatment, that deviates from the systematic behavior of the other cases (-> size increases in a non-systematic way).

The effects are:

  1. Space is wasted, when stateless objects (i.e. classes/structs with no members) are used
  2. Zero length arrays are forbidden.

Since one arrives at these problems very quickly when writing generic code, there have been several attempts for mitigation

  • The empty base class optimization. This solves 1) for a subset of cases
  • Introduction of std::array which allows for N==0. This solves 2) but still has issue 1)
  • The introcduction of [no_unique_address], which finally solves 1) for all remaining cases. At least when the user explicity requests it.
  • Introduction of std::is_empty. Needed because the obvious sizeof does not work (as sizeof(Empty) >= 1). (Thanks to Dwayne Robinson)

Maybe allowing zero-sized objects would have been the cleaner solution which could have prevented the fragmentation *). However when you search for zero-sized object on SO you will find questions with different answers (sometimes not convincing) and quickly notice that this is a disputed topic. Allowing zero-sized objects would require a change at the heart of the C++ language and given the fact that the C++ language is very complex already, the standard comittee likely decided for the minimal invasive route and just introduced a new attribute.

Together with the other mitigations from above it finally solves all issues due to disallowal of zero-sized objects. Even though it is maybe not be the nicest solution from a fundamental point of view, it is effective.

*) To me the unique-identity-rule for zero sized types does not make much sense anyway. Why should we want objects, which are stateless per programmers choice (i.e. have no non-static data members), to have an unique address in the first place? The address is some kind of (immutable) state of an object and if the programmer wanted a state they could just add a nonstatic data member.

Andreas H.
  • 5,557
  • 23
  • 32
  • To my mind, an object of size N should have been defined as having a total of N+1 distinct addresses, of which the first N would be pointers to bytes and the last N would be pointers "just past" bytes. No pointer to a byte within an object would share an address with any pointer to a byte in any other, and no pointer just past a byte in an object would share an address with a pointer just past a byte in any other, but a pointer to a byte in one object could share an address with a pointer just past a byte in an object. – supercat Jul 08 '20 at 14:35
  • An object of size zero would have an address, to/from which an offset of zero could be added or subtracted to yield the same address, or which could be subtracted from itself to yield zero, but the address would not be a pointer to or just past any byte, and may or may not arbitrarily compare equal to that of any other object. – supercat Jul 08 '20 at 14:36
  • 1
    @supercat: Not sure if I understand correctly. Essentially you say pointer arithmetic with zero size objects would work. There is one thing to consider: If you have an array of zero size types, e.g. `Empty e[5]`: The C++ idiomatic way of iterating over the elements `for (i = begin(e); i != end(e); i++)` will not produce 5 iterations, but 0. This may be unexpected. – Andreas H. Jul 08 '20 at 15:46
  • Think of `std::destroy()`: it would not run the correct number of times the destructor, which it probably should. But perhaps it would not be an issue to just disallow pointer arithmetic on pointers to zero size types. Also one could consider to disallowing pointer comparisons for pointers to zero size types (in that case `destroy_n` could be made to work and `destroy` would give a compile time error). Probably it is possible to come up with a consistent proposal for zero-size objects, but it might be a lot of work. – Andreas H. Jul 08 '20 at 15:49
  • 1
    One thing I also do not undestand is that why one wants to objects, which are stateless per programmers choice (they have no non-static data members), to have an unique address in the first place. The address is some kind of (immutable) state of an object and to me that makes not much sense anyway. – Andreas H. Jul 08 '20 at 15:52
  • I would only define pointer arithmetic on incomplete types in the cases of adding or subtracting zero, or subtracting a pointer from itself. The primary uses I see for zero-size objects would be to facilitate syntactic sugar behavioral couplings via inheritance, arrays which might or might not be needed based upon compile-time computations, or flexible-array-member-style constructs (a purpose for which such types were often supported prior to C89). – supercat Jul 08 '20 at 15:57
  • 1
    For example, if one needs a data structure that stores N things as N/256 objects that hold 256 each, along with an object that holds N%256 things, being able to treat multiples of 256 just like any other number would be more convenient than having to either exclude or pad the "tail" object when N is a multiple of 256. – supercat Jul 08 '20 at 16:06
  • BTW, in regard to your last question, I suspect that some programs in pre-standard days used empty objects as identity tokens, and the authors of C99 wanted neither to break such programs, nor define a new syntax to indicate whether an empty object might be used as an identity token. IMHO, the proper fix would have been to add an explicit "identity token" type which would have an address which is distinct from that associated with any other identity token but need not have any addressable storage there, and deprecate the use of empty objects for that purpose. On some platforms,... – supercat Jul 08 '20 at 17:01
  • ...address space is far more plentiful than actual storage, so a compiler and linker could cooperate to place identity tokens in unmapped regions of address space. – supercat Jul 08 '20 at 17:03
  • @AndreasH. Also unexpected would be the result of the canonical way of computing the number of elements in an array of zero-sized elements (`sizeof(arr)/sizeof(*arr)` ...) – Peter - Reinstate Monica Jul 08 '20 at 17:05
  • @Peter-ReinstateMonica The C++17 idiomatic `size(arr)` would work, though (or could be made to work). – Andreas H. Jul 08 '20 at 17:13
  • @supercat True, due to backward compatibility some explicit user intervention for zero-sizedness would be necessary. Then we probably arrive at some attribute nevertheless ... – Andreas H. Jul 08 '20 at 17:15
  • 1
    @Peter-ReinstateMonica: Most of the purposes that zero-sized objects would serve could be satisfied even if arrays of such objects, or arithmetic on pointers with such target types, were disallowed (given `int foo[0],*p=foo,*q=p+0;`, computation of `(q-p)/sizeof (*p)` wouldn't pose any problem). – supercat Jul 08 '20 at 17:20
  • 1
    I'd add [std::is_empty](https://en.cppreference.com/w/cpp/types/is_empty) as another work-around invented to mitigate this design choice. Give the stock `sizeof(SomeEmptyStruct)` doesn't return 0 as originally expected, I've written little helper functions with it for executing test cases on API's that validate the empty case too. – Dwayne Robinson Aug 04 '21 at 01:42