Follow

Keep Up to Date with the Most Important News

By pressing the Subscribe button, you confirm that you have read and are agreeing to our Privacy Policy and Terms of Use
Contact

[[no_unique_address]] in C++: Why doesn’t it allow overlap?

Learn why [[no_unique_address]] in C++ doesn’t merge objects of the same type and what the C++ standard says about overlapping addresses.
Illustration of C++ [[no_unique_address]] showing why same-type empty members cannot overlap with visual boxes labeled EmptyType and TagA/TagB, comparing memory layout constraints. Illustration of C++ [[no_unique_address]] showing why same-type empty members cannot overlap with visual boxes labeled EmptyType and TagA/TagB, comparing memory layout constraints.
  • 🧠 In C++20, [[no_unique_address]] lets members of empty types share memory. This does not happen if the members are of the same type.
  • ⚠️ Standard rules say that subobjects of the same type must have different addresses. This keeps object identity clear.
  • 🧩 [[no_unique_address]] helps make layouts smaller. But, it does not change the basic rules for aliasing and pointers.
  • 💡 When you use empty members of different types, they can share storage. This makes the layout smaller.
  • 🧰 Compilers like GCC and Clang follow ABI rules. This keeps layouts the same on different platforms.

Understanding [[no_unique_address]] in C++: Why Can't Same-Type Members Overlap?

If you have looked at layout changes in C++20 and later, you have likely seen [[no_unique_address]]. It is powerful and well-designed, but there is one confusing part: when you use this on two members of the same empty type, they still get separate addresses. Why is this? This article explains how [[no_unique_address]] works. It connects its behavior to C++'s object layout rules. And then, it shows how you can still use it well.


What Is [[no_unique_address]]?

[[no_unique_address]] came with the C++20 standard. It is a language attribute that tells the compiler it can make a non-static data member use less storage. This is especially true when that member is an empty class. The main goal is to let empty objects—which usually still take one byte of memory—use no memory. They can do this by sharing space with other data members.

Before C++20, C++ compilers mostly used Empty Base Optimization (EBO) to get similar results. This method lets empty base classes use no memory when you use them in inheritance. But, [[no_unique_address]] brings this optimization to member fields that are not base classes. This makes layout design simpler. And it lets you write clearer, more efficient code, especially in templates and containers.

MEDevel.com: Open-source for Healthcare and Education

Collecting and validating open-source software for healthcare, education, enterprise, development, medical imaging, medical records, and digital pathology.

Visit Medevel

At its heart, [[no_unique_address]] tells the compiler that the marked member does not need a separate memory address. This is true unless language rules require one. It allows some flexibility. But, [[no_unique_address]] does not mean every instance can be collapsed, especially when keeping objects distinct is important.


Basic Usage of [[no_unique_address]]

Using [[no_unique_address]] is simple to write and changes structure a lot. Here is how it looks in real use:

struct EmptyTag {};

struct Container {
    [[no_unique_address]] EmptyTag tag;
    int value;
};

EmptyTag is an empty type. This means it has no data members and no virtual functions. So, it usually takes at least one byte per instance. This happens to meet unique identity rules in C++. But, with C++20, [[no_unique_address]] lets the compiler reuse space. It can even put the tag field at the same address as another field. Or it can remove it completely from the layout, if allowed.

This method is useful in many situations. For example:

  • Strategy objects that hold no state.
  • Template traits.
  • Class designs based on policies.
  • Wrapper types that act like optional.

The main benefit of this attribute shows up when used in template classes. These classes often have helper objects, settings flags, or empty policy types. Without this attribute, these things would make the layout bigger than it needs to be.


Why Members of the Same Type Don’t Share Addresses

One of the most confusing rules for this attribute is that two members of the same empty type cannot share the same memory address. This is true even if they both have [[no_unique_address]].

This rule comes from the basic C++ object model. You can find it in subsection §11.6 of the C++20 Standard:

“Two subobjects of the same object must have different addresses if they are of the same type.”

This rule is in place to keep these things true:

  1. Object Identity: You can get the address of any subobject. So, &object.a and &object.b must be different when a and b are different members. This is true even if their content looks the same.
  2. Pointer Meaning: If &a == &b, it means the objects are the same. If we let this be false while objects still point to the same address, it would break many ideas of the language.
  3. Clear Meaning: If fields of the same type could share addresses, it would cause problems for debugging, optimizing, and understanding how code works.

Even empty objects must follow this rule. This is simply to meet the idea that you must be able to get a unique address for each member. This is true even if you see nothing stored there.

This rule greatly changes how compilers handle layout. It stops them from saving storage in these cases. This is true even if the storage seems extra.


Compiler Behavior and Why It Matters

All big C++ compilers, like GCC, Clang, and MSVC, support [[no_unique_address]]. But, they stick closely to the object layout rules. These rules are in the C++ standard and in ABIs specific to the target. For example, the Itanium C++ ABI is what GCC and Clang use for many platforms.

This means:

  • Same-type members do not overlap. This is true even with [[no_unique_address]].
  • Different-type empty members can be combined. This happens if nothing else stops it.

For example, given:

struct Empty {};
struct A {
    [[no_unique_address]] Empty a;
    [[no_unique_address]] Empty b;
};

Compilers give a and b separate addresses. This is true even though Empty is empty. This is not a strange behavior. Instead, it is how compilers follow the rules.

Why this is important:

  • It stops breaks of aliasing rules. Compilers enforce these rules when they optimize.
  • It makes sure debug tools can find and track each subobject by itself.
  • It keeps ABI compatible across different parts of the code and final programs.

So [[no_unique_address]] suggests that overlaps are okay. But, compiler backends must split same-type subobjects. They do this to follow the formal rules.


Code Example: Same Type vs Distinct Type Members

To show how this works, look at the code below:

#include <iostream>

struct Empty {};

struct SameTypeExample {
    [[no_unique_address]] Empty a;
    [[no_unique_address]] Empty b;
};

struct DistinctTypeExample {
    struct TagA {};
    struct TagB {};

    [[no_unique_address]] TagA a;
    [[no_unique_address]] TagB b;
};

int main() {
    SameTypeExample obj1;
    DistinctTypeExample obj2;

    std::cout << "SameTypeExample:\n";
    std::cout << "&obj1.a = " << &obj1.a << "\n";
    std::cout << "&obj1.b = " << &obj1.b << "\n\n";

    std::cout << "DistinctTypeExample:\n";
    std::cout << "&obj2.a = " << &obj2.a << "\n";
    std::cout << "&obj2.b = " << &obj2.b << "\n";
}

Example Output:

SameTypeExample:
&obj1.a = 0x7ffca3ee4c68
&obj1.b = 0x7ffca3ee4c69

DistinctTypeExample:
&obj2.a = 0x7ffca3ee4c6a
&obj2.b = 0x7ffca3ee4c6a  // same address - overlapping

This clearly shows that [[no_unique_address]] can combine storage for different empty types. But it does not do this when types are the same.

If you want to see the exact memory layout, this command with GCC helps:

g++ -std=c++20 -fdump-class-layout=file.cpp

This dump gives you details about the offset and alignment for each member in a struct.


EBO vs. [[no_unique_address]]

Empty Base Optimization (EBO)

EBO has been a common way in C++ to make empty base classes use less storage. For example:

template <typename T>
struct Wrapper : T {
    int data;
};

If T is empty, inheriting from it lets the compiler put it into the derived type with no memory usage. This works because base classes already have flexible storage rules. This makes them good for functions that hold no state, or for type tags.

How [[no_unique_address]] Improves the Pattern

With [[no_unique_address]], you do not need inheritance tricks or CRTP just to get empty members out of your layouts. You write a normal member. Then you add the attribute. And you get almost the same benefit:

template <typename U>
struct Wrapper {
    [[no_unique_address]] U base;
    int data;
};

You do not need inheritance. There are fewer complex parts. And the code's purpose is clearer. For code that uses many templates, this makes specializations easier to keep up. Also, it causes fewer problems from complicated inheritance setups.


Object Identity and Undefined Behavior

One key rule [[no_unique_address]] cannot break is object identity. Look at this code:

if (&obj.a == &obj.b) {
    // You might infer a == b, but modifying one affects both
}

If this happened with two different members of the same type, it would break aliasing rules. Compilers would have to stop optimizations, even if they were otherwise okay. This is because checking for aliases would become unreliable.

Also:

  • The One Definition Rule (ODR) needs each subobject to be unique.
  • Debuggers and tools that run code need objects to stay in the same places.
  • Doing math with pointers or using reinterpret_cast on Zero-Size Types that share space can lead to unexpected results.

So, the standard makes layouts careful. This is for safety.


Design Tips for Layout Optimization

Here are some useful ways to get the most out of [[no_unique_address]]:

  • Use wrapper types to break same-type constraints:

    struct Tag {};          // empty
    struct Wrapper1 { [[no_unique_address]] Tag x; };
    struct Wrapper2 { [[no_unique_address]] Tag y; };
    
  • Group flags that belong together. Use different Zero-Size Types for this, not tag types you can use again.

  • Do not worry too much about saving a single byte. Savings matter most in fast loops or large groups of data.

  • Use this with small "functor" patterns. This helps make inline storage better. For example, for function wrappers or callbacks.


The Road Ahead: Future Proposals?

The C++ language keeps changing to include more layout-aware programming. Groups like WG21 are looking into:

  • Adding more general layout reflection.
  • Making type safety better. And making it easier to look into layout ideas.
  • Connecting to more advanced ideas, like Rust's special Zero-Size Type optimizations.

Right now, there are limits. But future C++ versions might let same-type empty members overlap. This could happen with optional flags. Or with stricter aliasing controls that use constexpr annotations.


Known Quirks and Limitations

[[no_unique_address]] is clever, but it has some warnings:

  • Does not work for References: References always need storage.
  • ⚠️ Changes POD and Standard Layout: Using this attribute can break how well your code works with C.
  • 💥 Not fully supported by all tools: Some static analyzers and debuggers might misunderstand members that use no memory.

Always check your structural ideas on real systems.


Real-World Use Case: Compact Optional-Type Design

Here is a good use for it: an OptionalLike<T, Flag> wrapper. It provides compact optional behavior:

template <typename T, typename Flag>
struct OptionalLite {
    [[no_unique_address]] Flag marker;
    T value;
};

If Flag is an empty policy type, you save a lot of space. Use this pattern when designing allocators. Or in small functional containers. And also for customizing policies at compile-time.

The standard library is working on proposals. These include std::compressed_pair and std::optional<T, Policy> versions. They are adding these kinds of optimizations.


Performance Payoff: When It Matters

Do not think saving a single byte is unimportant. In systems with little memory or where speed is key, big effects include:

  • ✅ Data packed tightly in containers. This means better L1/L2 cache use.
  • ✅ Memory areas are dense. They use fewer pages.
  • ✅ Snapshots are smaller. And network data transfers are lighter.

Using it without care can be risky. But, using it in specific ways in modern C++ core system designs can cut down on overhead a lot.


Best Practices Recap

  • ✅ Use [[no_unique_address]] for data members that are empty and not critical.
  • ❌ Do not use this with members of the same type if you expect them to overlap. This will not happen.
  • ☑️ Use type wrappers to make empty types different. This lets them overlap if you want.
  • ⚠️ Do not use the attribute too much. Do not use it in ways that make object identity unclear. And do not use it outside of applications that work with PODs.

Final Thoughts

[[no_unique_address]] is a useful tool for making layouts smaller and code use less memory. It opens new ways for lean designs. But, it does not change the basic object identity and memory rules in the C++ object model. You can use this feature well in your modern C++ designs. Just understand how it works and its limits, especially for same-type fields.


Citations

Add a comment

Leave a Reply

Keep Up to Date with the Most Important News

By pressing the Subscribe button, you confirm that you have read and are agreeing to our Privacy Policy and Terms of Use

Discover more from Dev solutions

Subscribe now to keep reading and get access to the full archive.

Continue reading