Brief note on terminology that is fine for describing an element of a class design, but not useful when deciding how to design a class.

Const and pointers

When you have an int variable, const can be used before or after the type, it’s just a matter of taste, it does not make a difference to the type of the variable. You can’t change the value for a const int, but you can take a (non const copy and change the copy as much as you want to:

1
2
3
4
5
6
7
8
9
10
const int some_int = 41;

// equivalent to:
// int const some_int = 41;

// would get an error:
// some_int = 42;

int another_int = some_int;
another_int = 42;

When you have a pointer however, it does matter if the const is before or after the *, you can have both or either. The const preceding * refers to the pointed type: can’t change the pointed value. The const, following * refers to the value of the pointer: can’t change the pointer itself, but can take a (non const) copy, and change the copy as much as we want to (to point to something else).

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
const int * const some_ptr = &some_int;

// equivalent to:
// int const * const another_ptr = &some_int;

// pointer to const int, pointed value is const
const int * ptr_to_const = some_ptr;
// can't do this:
// *ptr_to_const = 42;
// or this
// *some_ptr = 42;
// but can do this:
ptr_to_const = nullptr;

// pointer to int, pointer is const
int * const ptr_const = &another_int;
// can't do this:
// ptr_const = nullptr;
// or this:
// some_ptr = null_ptr;
// but can do this:
*ptr_const = 42;

Examples

std::vector

std::vector typically stores three pointers to manage the start , the used and the available space of the dynamically allocated array.

Despite these pointers, when you put const in front of a vector, it behaves like an int: can’t change the values in the vector. When you take a copy of the vector it copies all the values of the vector, not just the three pointers. When you compare two vectors, it compares the values, not the three pointers.

One shortcut is to say that vector propagates const deep, does a deep copy, implements deep comparison.

The deep const behaviour is why we use way more often const std::vector<int> than std::vector<const int>.

std::shared_ptr

std::shared_ptr however has pointer semantics: const std::shared_ptr<int> means can’t change this pointer, but can change the pointed value, copy points to the same thing as the original rather than copy the pointed value, comparison compares the underlying pointer.

We say that std::shared_ptr uses shallow `const, shallow copy and shallow comparison.

std::string_view and std::span

std::string_view and std::span provide views into contiguous sequences. To do that they each typically store two pointers.

They copy shallow (like pointers), but then they diverge.

std::string_view offers a immutable view, so const std::string_view, or not const, the view is constant, to say that constness propagates deep is somehow incorrect: it’s always deep const. std::span offers a mutable view, so const std::span<int> is shallow i.e. can’t change the pointers, but can change the pointed values; use std::span<const int> to make the view constant.

Comparison operations of std::string_view perform a deep comparison, comparing the pointed sequence. std::span does not implement comparisons.

Discussion

Using terminology like deep or shallow is only useful as a quick descriptive word, but it does not offer any clue into why the examples above were designed that way and what is good choice for a new type. A better insight is given by the thought that good types behave properly. One reasonable expectation is the following:

Axiom: If you take a copy and change the original, then the copy is different from the original

This is only one of the many expectations of a regular concept in particular. Even if you design a class that’s not regular, it’s often better to not implement behaviour that would lead to confusing behavior (e.g. not implement copy, move, comparisons) than to implement it and contradict axioms like the one above.

When we look at it this way, it makes sense that std::vector does deep copy, const and comparisons: it’s like an int. It makes sense that std::shared_ptr does it shallow: it’s like a pointer. std::string_view gets away with a combination of shallow copy and deep compare because it’s a constant view: the deep compare is debatable, but it’s handy as well. std::span does not get away with a combination of shallow and deep because it’s not a constant view; a combination of shallow copy, shallow const and deep compare would not meet the axiom above.