Ch25.4: Declarations with Sharp Edges
1. explicit(bool): Conditional Explicitness
A constructor can be explicit or not, and the choice is usually made once for
the type. C++20 allows the choice to depend on a compile-time condition:
#include <type_traits>
template <typename T>
struct box
{
T value;
explicit(std::is_integral_v<T>) box(T v) : value{v} {}
};
The expression in explicit(...) must be a constant expression of type
bool. When it is true, the constructor is explicit; when it is
false, it is converting. The usual use is a wrapper that should accept implicit
conversions for some element types but not for others:
box<double> d = 1.0; // OK: explicit(false)
box<int> i = 1; // error: explicit(true)
box<int> j{1}; // OK: direct initialization
This is not a runtime check. The condition is evaluated during template instantiation, and the resulting constructor is either explicit or not. It is useful for generic wrappers, strong typedefs and unit types where the conversion policy depends on the payload.
Do not use explicit(bool) to encode a runtime policy. If the decision depends on
a value, it is not a conversion rule; write a named factory function instead.
2. [[no_unique_address]]: Empty Members
An empty class still has a size of at least one byte, because every object needs a distinct address. A member of an empty type normally costs that byte plus padding:
struct empty {};
struct without
{
empty e;
int x;
};
struct with
{
[[no_unique_address]] empty e;
int x;
};
[[no_unique_address]] tells the compiler that the member does not need a unique
address, so it may overlap with other members. On the implementations used in this book,
sizeof(without) is usually 8 and sizeof(with) is usually 4. The
standard permits the overlap; it does not require a specific size.
static_assert(sizeof(with) <= sizeof(without));
This is how a stateless allocator, comparator or deleter can be stored in a container without growing it. The attribute is a layout permission, not a guarantee: the compiler may still give the member an address if it needs one, and the layout can differ between ABIs.
Do not apply it to a member whose address identity matters. If code takes
&obj.e and compares it with another object's address, overlapping storage can
break the assumption. Use it for policy objects that are genuinely stateless.
3. alignas and alignof
alignof(T) asks for the alignment requirement of a type.
alignas(N) requests a stricter alignment for an object or a type:
#include <cstddef>
struct alignas(64) cache_line
{
std::byte data[64];
};
static_assert(alignof(cache_line) == 64);
static_assert(sizeof(cache_line) == 64);
alignas can increase alignment; it cannot reduce it below the type's natural
requirement. A type with alignas(64) is 64-byte aligned wherever it is used, and
the compiler inserts padding in arrays and in containing structs to preserve that.
Alignment is not the same as size. A 64-byte-aligned object can be smaller or larger than 64 bytes; the alignment only fixes the starting address. Over-aligning every object wastes memory; use it for cache-line separation, SIMD, memory-mapped I/O or a documented ABI, not as a general performance hint.
The alignment of a type is also why a union's alignment is the strictest alignment among its members, and why the order of members in a struct can change its size. Layout is a first-class part of the ABI.
4. offsetof
offsetof(type, member) gives the byte offset of a member from the start of a
standard-layout object:
#include <cstddef>
struct record
{
int id;
double value;
};
static_assert(offsetof(record, id) == 0);
static_assert(offsetof(record, value) >= sizeof(int));
offsetof is only defined for standard-layout types. A type with virtual
functions, virtual bases, private data members in a non-standard-layout arrangement, or
multiple access sections may not be usable. It is a compile-time macro, not a runtime
reflection call; it does not know about a particular object.
C++26 reflection provides std::meta::offset_of
(Ch24.3) for the same question over a
reflection. The macro is still the portable answer for a fixed, standard-layout struct.
Offsets are ABI-dependent. A static assertion on an offset documents the layout your code expects; it does not make the layout portable. If the offset must match a wire format, use a serialization layer with explicit widths and endianness, not a raw struct.
5. volatile Is Not Synchronization
volatile tells the compiler that every access to the object is observable and
must not be optimized away or reordered with respect to other volatile accesses. It does not
provide atomicity, mutual exclusion or inter-thread ordering.
volatile std::uint32_t *mmio = reinterpret_cast<volatile std::uint32_t *>(0x40000000u);
*mmio = 1;
That is the legitimate use: memory-mapped I/O, a hardware register, or a variable modified by a signal handler. In those cases the compiler must actually perform the load or store.
This is the illegitimate use:
volatile int counter{};
++counter; // still a data race if another thread does the same
volatile does not make ++ atomic. Two threads can still read the
same value, increment it and write it back. Use std::atomic<int> for
inter-thread communication, and use a mutex when the protected state is more than one word.
volatile is for the hardware and the signal handler, not for the scheduler.
6. A Few More Sharp Edges
| Declaration | What it actually means |
|---|---|
constexpr member function |
Callable in a constant expression; it does not make the object immutable |
mutable member |
Modifiable even in a const member function; use for caches and mutexes |
static member function |
No this; it cannot access non-static members without an object |
explicit conversion operator |
Requires a cast or a contextual conversion; it blocks implicit conversions |
[[nodiscard]] |
A warning if the return value is ignored; it does not change the type |
[[fallthrough]] |
Documents an intentional switch fallthrough; it does not change control flow |
The common thread is that a declaration can carry a rule about initialization, layout,
linkage, conversion or diagnostics. Read the rule, not the keyword's reputation. A
constexpr function is not automatically faster; an inline variable
is not automatically inlined; volatile is not atomic.
Key Takeaways
explicit(bool)makes explicitness depend on a compile-time condition.[[no_unique_address]]permits empty members to overlap; it does not guarantee a size.alignasraises alignment;alignofasks for it; size and alignment are different.offsetofis for standard-layout types and is ABI-dependent.volatileis for hardware and signals, not for thread synchronization.- Read what a declaration promises; the keyword's reputation is not the rule.
That closes the fine-print chapter. The features here are not new language directions; they are the rules that were already in the language but rarely got a page of their own.