﻿# C\+\+ programmer's guide to undefined behavior: part 4 of 11

Your attention is invited to the fourth part of an e\-book on undefined behavior\. This is not a textbook, as it's intended for those who are already familiar with C\+\+ programming\. It's a kind of C\+\+ programmer's guide to undefined behavior and to its most secret and exotic corners\. The book was written by Dmitry Sviridkin and edited by Andrey Karpov\.

![1156_book_pt_4/image1.png](https://import.viva64.com/docx/blog/1156_book_pt_4/image1.png)

## Errors in object lifetime: lambda function capture lists

C\+\+11 has given us lambda functions, as well as a new way to implicitly get dangling references\.

A lambda function that captures something by reference is safe as long as it is not returned anywhere outside the creation scope\. As soon as we return or save a lambda somewhere, the real fun begins:

```cpp
auto make_add_n(int n) {
    return [&](int x) {
        return x + n;      // n becomes a dangling reference!
    };
}

...
auto add5 = make_add_n(5);
std::cout << add5(5);      // UB!
```

This isn't groundbreaking, as we can see all the same issues as returning a reference from a function\. Clang can sometimes [issue a warning](https://godbolt.org/z/6sWE8jdEd)\.

<details>
   <summary>The UB emergence:</summary>

The code above is compiled using GCC 14\.1 \(\-O3 \-std\=c\+\+20\)\. It outputs a value of 5\.

If we build the code using Clang 18\.1 \(\-O3 \-std\=c\+\+20\), the result is 1711411576\. Here's the warning:

```cpp
<source>:5:13: warning:
address of stack memory associated with parameter 'n' returned
    5 |     return [&](int x) {
      |             ^
<source>:6:20: note: implicitly captured by reference due to use here
    5 |     return [&](int x) {
      |             ~
    6 |         return x + n;
      |                    ^
```


</details>
However, once we take the _make\_add\_n_ argument by reference:

```cpp
auto make_add_n(const int &n) {
    return [&](int x) {
        return x + n; // n becomes a dangling reference!
    };
}
```

Both [compilers stay silent](https://godbolt.org/z/qMn8e5jb4):

1. The result when building GCC 14\.1 \(\-O3 \-std\=c\+\+20\): 5;
1. The result when building Clang 18\.1 \(\-O3 \-std\=c\+\+20\): 10\.

We [can create](https://godbolt.org/z/dYGbKs83j) a similar issue for member functions:

```cpp
struct Task {
  int id;

  std::function<void()> GetNotifier() {
    return [this]{
      // this may become a dangling reference!
      std::cout << "notify " << id << "\n";
    };
  }
};

int main() {
  auto notify = Task { 5 }.GetNotifier();
  notify(); // UB!
}
```

<details>
   <summary>The UB emergence:</summary>

* GCC 14\.1 \(\-O3 \-std\=c\+\+20\): "notify 0";
* Clang 18\.1 \(\-O3 \-std\=c\+\+20\): "notify 29863"\.


</details>


However, in this example, we can see _this_ in the capture list and, naturally, get a bit concerned\. Before C\+\+20, you could [shoot yourself in the foot](https://godbolt.org/z/WExKPo) a little less explicitly:

```cpp
struct Task {
  int id;

  std::function<void()> GetNotifier() {
    return [=]{
      // 'this' may become a dangling reference!
      std::cout << "notify " << id << "\n";
    };
  }
};
```

The _\=_ symbol requires capturing everything by value\. However, it's not the _id_ data member that is captured, but the _this_ pointer\.

If you see a lambda that has _this_, _\=_ \(up to C\+\+20\), or _&_ in its capture list, be sure to check how and where that lambda is used\. Add overloads for checking the lifetime of captured variables\.

```cpp
struct Task {
  int id;

  std::function<void()> GetNotifier() && = delete;

  std::function<void()> GetNotifier() & {
    return [this]{
      // it's harder for this to become a dangling reference now
      std::cout << "notify " << id << "\n";
    };
  }
};
```

If possible, it's better to use capture by value or move initialization capture instead of capture by reference\.

```cpp
auto make_greeting(std::string msg) {
  return [message = std::move(msg)] (const std::string& name) {
    std::cout << message << name << "\n";
  };
}
...
auto greeting = make_greeting("hello, ");
greeting("world");
```

#### Useful links

1. Cppreference\. [Lambda expressions](https://en.cppreference.com/w/cpp/language/lambda)\. 
1. Avinash\. [Modern \(Effective\) C\+\+ \- Avoid default capture modes](http://cppatomic.blogspot.com/2018/03/modern-effective-c-avoid-default.html)\.
1. SEI CERT C\+\+ Coding Standard\. [EXP61\-CPP\. A lambda object must not outlive any of its reference captured objects](https://wiki.sei.cmu.edu/confluence/display/cplusplus/EXP61-CPP.+A+lambda+object+must+not+outlive+any+of+its+reference+captured+objects)\.

## Errors in object lifetime: tuples that shoot at your feet

Since C\+\+11, there's a wonderful _std::tuple_ class template in the standard library\. This is a tuple, a heterogeneous list, both great and helpful\. Except that creating a tuple without breaking anything while still getting exactly what you wanted is a really non\-trivial task\.

Explicitly specifying element types of a very long container isn't a fun thing to do\.

C\+\+11 has given us three ways to save on type specification \(different functions for creating tuples\):

* _make\_tuple_;
* _tie_;
* _forward\_as\_tuple_\.

C\+\+17 also makes it possible to use class template argument deduction and just write like that:

```cpp
auto t = tuple { 1, "string", 1.f };
```

All this great variety enables us to fine\-tune what types we want in container elements exactly: references or not\. It also enables the possibility of making a mistake and run into a lifetime issue\.

The _std::make\_tuple_ function template removes references, decays arrays to pointers, and removes _const_\. Basically, it applies _std::decay\_t_\.

There's a special case, as usual, born out of good intentions\.

If the type of the _make\_tuple_ argument is _std::reference\_wrapper<T\>_, it's [converted](https://godbolt.org/z/bv17q5fEM) to _T&_ in the tuple:

```cpp
int x = 5;
float y = 6;
auto t = std::make_tuple(std::ref(x), 
                         std::cref(y), 
                         "hello");
static_assert(std::is_same_v<decltype(t), // The code compiles
              std::tuple<int&, 
                         const float&, 
                         const char*>>);
```

The class template argument deduction [doesn't consider](https://godbolt.org/z/cEd3e69bj) the special case of _std::reference\_wrapper_\. However, the decay occurs\. The following code also compiles:

```cpp
int x = 5;
float y = 6;
auto t = std::tuple(std::ref(x), std::cref(y), "hello");
static_assert(std::is_same_v<decltype(t), 
                             std::tuple<std::reference_wrapper<int>,
                                        std::reference_wrapper<const float>,
                                        const char*>>);
```

The _std::forward\_as\_tuple_ function always constructs a tuple of references\. So, you can get a [reference to a dead temporary object](https://godbolt.org/z/8c8EjGq7c):

```cpp
int x = 5;
auto t = std::forward_as_tuple(x, 6.f, std::move("hello"));
static_assert(
  std::is_same_v<
    decltype(t), 
    std::tuple<int&,
               float&&,
               const char (&&) [6]>>); // This is the rvalue reference
                                       // to an array 
std::get<1>(t); // UB!
```

The _std::tie_ function constructs a tuple only from _lvalue_ references\. It's harder to fail with it, but still [possible](https://godbolt.org/z/WPP7qca6a) if you want to return the resulting tuple from a function\. However, this case is quite similar to the cases of returning any references from functions:

```cpp
template <class... T>
auto tie_consts(const T&... args) {
  return std::tie(args...);
}

int main(int argc, char **argv) {
  auto t = tie_consts(1, 1.f, "hello");
  static_assert(std::is_same_v<decltype(t),
                               std::tuple<const int&, 
                                          const float&, 
                                          const char (&)[6]>>);
  std::cout << std::get<1>(t) << "\n"; // UB
}
```

Here are some general recommendations:

1\. To create result tuples, use _make\_tuple_ with explicit cref/ref or use a constructor if references aren't needed\.

2\. Use _std::tie_ only to temporarily represent a set of variables as a tuple:

```cpp
std::tie(it, inserted) = map.insert({x, y});  // tuple unpacking
std::tie(x1, y1, z1) == std::tie(x2, y2, z2); // component-wise comparison
```

3\. Use _std::forward\_as\_tuple_ only when passing arguments\. Don't save the result tuple anywhere\.

Here's also a bonus\.

Die\-hard Python fans may want to try using _std::tie_ to swap variable values:

```cpp
// x, y = y, x
int x = 5;
int y = 3;
std::tie(x, y) = std::tie(y, x);
std::cout << x <<  " " << y;
```

This isn't Python, though\. So, the code behavior is not defined but cheer up\. It's only _unspecified_\. As a result, you get either _5 5_ or _3 3_\.

## Errors in object lifetime: unexpected mutability

It was a warm spring day\. Sipping my tea, I slowly and lazily flipped through student projects\. I could've said that nothing seemed to be going wrong, but unfortunately the papers were done in C\+\+\.

Suddenly, I noticed an innocuous string used for diagnostic logging:

```cpp
printf("from %s -- to %s",
       storage[from].value.c_str(), storage[to].value.c_str());
```

There's nothing wrong with it, right? Although, at that moment I was overcome with terror\. Let me share it with you now\.

In this string, lurks an incredible opportunity for bugs, unexpected crashes, and undefined behavior\!

Every line of code in C\+\+ is highly dependent on its context\. What can we assume just by looking at this _printf_?

1. _storage_ is some kind of an associative container;
1. This _storage_ stores elements that seem to have the _value_ string data member\. It is likely of the _std::string_ type;
1. The programmer who wrote this _printf_ probably assumes that both _from_ and _to_ keys are in the container\.

Good\. Now meet the bad part: the last assumption could be accidentally violated anytime in the future life of the codebase\. And violating this assumption leads to the most surprising consequences\! They'll be all the more surprising if this _printf_ is hidden under a macro and exists only with specific compilation options\. For example, if the maximum logging level is set at compile time\.

### These different containers

If _from_ or _to_ isn't in the list of _storage_ keys, it all depends on how _storage_ handles access to the missing key\. To do that, we need to see what type _storage_ has:

* if it's an array or a vector, then hello, array overrun and undefined behavior;
* if it's _std::map_ or _std::unordered\_map_, then you got lucky today: your default constructor was called, and you got empty strings\. It's probably not what you wanted, though, and the newly created element will break your code somewhere\.

Is that it? Aren't we forgetting something?

We forgot that it's not limited to standard STL containers\. Containers can be from other libraries, too\. This is very common when it comes to associative containers\. Due to the standard requirements for stability of element references and iteration guarantees, the _std::unordered\_map_ class can't be implemented efficiently\. It's cache\-unfriendly and [almost always](https://tessil.github.io/2016/08/29/benchmark-hopscotch-map.html) loses in benchmarks\. So, real\-world applications often use alternative implementations that neglect one or another of the guarantees\.

A popular option is the family of "flat" hash tables with open addressing\. All elements are stored in one continuous memory section\. Obviously, if there's no room for a new element in this section, the memory should be reallocated to insert it\. And the stability of references to elements is out of the question\.

Now let's get back to our code snippet again:

```cpp
printf("from %s -- to %s",
       storage[from].value.c_str(), storage[to].value.c_str());
```

If _storage_ is a hash table with similar behavior and interface to the standard one \(e\.g\. [abseil::flat\_hash\_map](https://github.com/abseil/abseil-cpp/blob/master/absl/container/flat_hash_map.h)\), a call via _operator\[\]_ modifies the container\. Depending on how full the table is and the presence of keys, different options await us\. Yet we need to boil them all down to one question: what key will cause the table to be reallocated when accessed?

Don't rush to think about _from_ and _to_, though, because the order in which the function arguments are calculated isn't specified\! The keys can be accessed in ANY order\! That only adds to the spice of the bug investigation if you encounter it in your work\.

But I'll let myself think that in our case it's _from_ that's accessed first, and then _to_\.

The option of both keys missing is equivalent to the option of only the _to_ key missing\. So, let's keep it that way\.

```cpp
auto& from_value = storage[from].value; // (1)
auto& to_value = storage[to].value      // (2)
```

* \(1\) returns a reference to a field in an existing or newly created element, everything's fine\. Even if we take _c\_str\(\)_, nothing bad happens either\. The container manages memory, there are no dangling pointers;
* \(2\) if _to_ is missing, then the container is either reallocated or not\. If the container isn't reallocated, the bug remains undetected\. Otherwise, the _from\_value_ reference is invalidated\!

### Short strings and long bugs

Is this a victory? Do we have the bug all sorted out?

Actually, no\. The call to _c\_str\(\)_, which is present in the original string, was deliberately omitted above\. It allowed the bug to go unnoticed and not mess up our tests\! It's all due to [SSO, small string optimization](https://pvs-studio.com/en/blog/terms/6658/)\.

If _storage\[from\]\.value_ is of the _std::string_ type, then most modern implementations would only experience the dreaded crash when using short strings\!

Simplified _std::string_ looks like this:

```cpp
class string {
  char* data;
  size_t data_size;
  size_t capacity_size;

  const char* c_str() const { return data; }
};
```

This is 3 \* 8 bytes on a 64\-bit platform\. And those strings lie in a heap\. This is an incredible waste if the string is very short \(0\-15 characters\)\! So, with enough effort and persistence, it's possible to use _union_ to make the structure look like this for short lines, for example:

```cpp
class string
{
  size_t capacity;

  union
  {
    struct 
    {
      char *ptr;
      size_t size;
    } heapbuf;

    char stackbuf[sizeof(heapbuf)];
  };
  const char* c_str() const {
    if (capacity > sizeof(heapbuf))
      return heapbuf.ptr;
    else
      return stackbuf;
  }
};
```

In the new implementation, we can arrange strings of small length within an object in _stackbuf_ without allocating a buffer on the heap\. Based on the _capacity_ data member, it's determined where characters are stored:

* if _capacity_ exceeds the size of _stackbuf_, the object manages a buffer on the heap and stores characters there;
* otherwise, the characters are stored in the object\.

Once again, we're back to extracting strings:

```cpp
const char* from_value = storage[from].value.c_str(); (1)
const char* to_value = storage[to].value.c_str();
```

\(1\) is a pointer to the data in the heap or in the string structure? Who knows, really\! 

If _from\_value_ points to the heap, and the container efficiently uses [move semantics](https://pvs-studio.com/en/blog/terms/6514/), then the string is moved\. The pointer is simply copied, and _from\_value_ remains valid\.

Otherwise, the string is copied, and almost certainly the _storage\[from\]\.value\.c\_str\(\)_ pointer doesn't equal to _from\_value_\.

Although, there's a slim chance that the reallocation was implemented via _realloc_, and we were so miraculously lucky that it was just enough to move the memory block boundary in _realloc_\.

What conclusions can we draw from all this?

1. C\+\+ is scary\. We see an 80 character long string, and when the debugger points to a crash there, we need to look at the order of argument evaluation, the container structure, the move semantics, and the structure of the container elements to determine what caused the crash;
1. The issue would be much less severe if _operator\[\]_ didn't modify the container;
1. One should be extremely careful with any refactoring in C\+\+\. Even if it's a simple replacement of one data structure with another that has exactly the same interface: our bug is hidden when we use _std::map/std::unordered\_map_, but it emerges with other tables\.

### How can we fight it?

I don't know of any static analyzer settings that would help here\. Only a thorough testing process can reveal such bugs\.

<details>
   <summary>A note on the topic of static code analysis\\\.</summary>

**Andrey Karpov:**

— I've written out some ideas on how to enhance PVS\-Studio to detect errors of the described type\. However, it's clear that the diagnostic rule would be inefficient, since it would work only for a limited set of simple cases\. We need to know the exact value of the item we are looking for and what the container is filled with\. This is a very challenging task for static analyzers, both in terms of analyzing the data flow and the computational cost of such an analysis\. So, I agree with Dmitry that a programmer should rely only on their own vigilance when writing and testing code\.


</details>
We can prevent such bugs by changing the way we write code\. I strongly advise you to try programming in Rust \(even if you won't use it in your work project\) to develop the habit of writing code that meets the requirements of its borrow checker\.

In C\+\+ code, if we guarantee that a container and the data in it can either have only _const_ references or no more than one mutable reference at a time, the error becomes almost impossible\. We can't guarantee it, though\. However, we can set a restriction that specifies all references to be constant:

```cpp
const auto& const_storage = storage;

// operator[] unavailable due to const
const auto& from_value = const_storage.at(from).value;

// operator[] unavailable due to const
const auto& to_value = const_storage.at(to).value;

// If any of the keys are missing, an exception is thrown
```

## Errors in object lifetime: proxy objects and implicit references

We really like generic code here in C\+\+\. And not only in C\+\+\. It's convenient, reusable, and flexible\. That's what templates are for\!

Let's write some generic code:

```cpp
template <class T>
auto pop_last(std::vector<T>& v) {
  assert(!v.empty());
  auto last = std::move(v.back());
  v.pop_back();
  return last;
}
```

It's quite wise to have such a function, because the existing _pop\_back_ returns _void_ in many containers\.  In reality, this is very inconvenient because most of the time we want to take out the last container element and do something with it, not just throw it away\.

Is this function okay? Of course, there will be undefined behavior on an empty vector, but we've written _assert_, so it's up to the user to handle that\. Just make sure to write the correct code and don't write the incorrect one\.\.\. Exception guarantees also raise questions, as the standard _pop\_back\(\)_ doesn't return anything because of them\. However, this is a topic for another chapter\. And everything else seems fine, right?

Well, let's use this function\!

```cpp
std::vector<bool> v(65, true);
auto last = pop_last(v);
std::cout << last;
```

Is everything okay? Looks like it is\. There's no crash\. We can use different compilers to [check it](https://godbolt.org/z/7ovG8qEhd)\. Is there really no catch?

Actually, there is one\. The number 65 was chosen for a reason, and most likely \(it depends on the implementation\) there's undefined behavior in the code that doesn't show up in any way, because that's how destructors of trivial types work\. Well, one thing at a time\.

### Proxy pattern and proxy objects

We won't go into detail about different design patterns\. There are [good books](https://en.wikipedia.org/wiki/Design_Patterns) for that\. All in all, a Proxy is an object that intercepts calls to another object with the same \(or similar\) interface to do something\. What exactly it'll do depends on the particular task and implementation\.

The C\+\+ standard library contains a variety of proxy objects \(sometimes not pure proxies but with additional features\):

* _std::reference\_wrapper_;
* _std::in\_ptr_ and_ std::inout\_ptr_ in C\+\+23;
* _std::osyncstream_ in C\+\+20;
* arithmetic operations on [_valarray_](https://en.cppreference.com/w/cpp/numeric/valarray) can return proxy objects;
* _std::vector<bool\>::reference_\.

The last one's the one we need\.

In the C\+\+98, the committee made a terrible decision that seemed reasonable at the time\. They created a specialization for _std::vector<bool\>_\. Normally, _sizeof\(bool\) \=\= sizeof\(char\)_, but one bit is enough for _bool_\. However, 99\.99% of all possible platforms can't address memory one bit at a time\. Let's pack bits in _vector<bool\>_ and store _CHAR\_BIT_ \(usually 8\) boolean values in one byte \(_char_\) for more efficient memory utilization\.

As a result, one needs to work with _std::vector<bool\>_ in a very special way:

* it's impossible to take an address \(pointer\) to a specific element in it;
* adjacent elements overlap;
* _reference_ isn't _bool&_;
* when accessing elements, _bool_\-like proxy objects \(knowing which bit in a byte to access\) are used\. That means we need to be careful with type inference\.

The_ reference_ type for _vector<bool\>_ looks like this:

```cpp
class reference {
public:
  operator bool() const { return (*concrete_byte_ptr) & (1 << bitno); }
  reference& operator=(bool) {...}
  ....
private:
  uchar8_t* concrete_byte_ptr;
  uchar8_t  bitno;
}
```

In the following line:

```cpp
auto last = std::move(v.back());
```

The _auto _type deduction doesn't deduce the references, but it applies only to built\-in C\+\+ references\. _T&_ and _T&&_ turn into _T_\. The _reference_ type doesn't turn into _bool_ by itself, even though there's an implicit _operator bool_\!

So, this is what we have:

```cpp
auto pop_last(std::vector<bool>& v) {
  // v.size() == 65
  auto last = std::move(v.back());
  // last is vector<bool>::reference; != bool
  v.pop_back();
  // v.size() == 64
  // We threw the last uint8/uint32/uint64
  // (implementation-dependent) from the vector.
  // last still refers to the thrown element.
  // If vector<bool> called (pseudo)destructor
  // when throwing this element
  // then when accessing this element via last, we violate
  // the C++ object model by accessing destroyed object -> UB.
  return last;
}
```

However, we didn't notice it when running the code because of the following things:

* _pop\_back_ doesn't reallocate the internal buffer of the vector;
* _\~bool_ does nothing\.

If we get an element from _pop\_last\(\)_, save it, and do something else to the vector that causes the buffer to be reallocated, then UB will emerge\.

### There are more surprises on the way\!

```cpp
int main() {
  std::vector<bool> v;
  v.push_back(false);
  std::cout << v[0] << " ";
  const auto b = v[0];
  auto c = b;
  c = true;
  std::cout << c << " " << b;
}
```

The code [outputs](https://godbolt.org/z/39a5cY47a) _0 1 1_\. Despite _const,_ the _b_ value has changed\. Well, this is obvious, isn't it? Since _b_ isn't a reference, but an object that behaves like a reference\!

This code becomes even more surprising and interesting in C\+\+23: if authors of [cppreference](https://en.cppreference.com/w/cpp/container/vector_bool/reference) didn't make a mistake when copying the updates from the standard, the assignment operator overloading via _const reference &_ is waiting for us\. We can even do this:

```cpp
int main() {
  std::vector<bool> v;
  v.push_back(false);
  std::cout << v[0] << "\t";  // 0
  const auto b = v[0];
  b = true;
  std::cout << v[0];          // 1
}
```

This behavior is quite defined but may be unexpected if you are writing all\-purpose template code\. Experienced C\+\+ programmers are cautious about the explicit use of _vector<bool\>_\.\.\. Do they always check in the template function that accepts _vector<T\>_ if _T \!\= bool_, though? They probably almost never do \(unless they're writing a public library\)\.

All right, we're done with the vector\. Everything else is fine, right?

Sure\!

Let's take a completely innocent function \(thanks [@sgshulman](https://github.com/sgshulman) for the example\):

```cpp
template <class T>
T sum(T a, T b)
{   
  T res;
  res = a + b;
  return res;
}
```

And we accidentally put\.\.\. that's right, some proxy type in there \(what could it be?\):

```cpp
std::vector<bool> v{true, false};
std::cout << sum(v[0], v[1]) << std::endl;
```

If lucky, we get a compilation error\. For example, in the MSVC implementation, _vector<bool\>::reference_ has no default constructor\. GCC and Clang might compile [the thing](https://godbolt.org/z/x1T5d9veY) that crashes with memory access errors: _T res_ refers to a non\-existent vector\.

We should also note how unexpectedly implicit calls to type conversion operators work here\! After all, there's no _operator\+_ defined in _vector<bool\>::reference_\. And _return a \+ b;_ doesn't compile\. Here, _a_ and _b_ are cast to _bool_, then to _int_ to be summed, and then back to _bool_\.

### More on the topic and why one needs to be vigilant

The _std::vector<bool\>_ is the best\-known example of an object that generates a proxy\. You can always write your own class and, if it emulates the trivial type behavior, it may amuse someone \(your colleagues, for example\)\.

The standard may allow to return the proxy for other types and operations\. Developers of a standard library may leverage this\. Or they may not\. In any case, we may accidentally or intentionally write code that behaves differently depending on the library version\. For example, according to the documentation, the _operator\*_ of _std::valarray_ in [_libstdc\+\+ v12\.1_](https://gcc.gnu.org/onlinedocs/gcc-12.1.0/libstdc++/api/a01589.html#ga51238588f2e0972914177fd7f9a12e15) and [Visual Studio 2022](https://learn.microsoft.com/en-us/cpp/standard-library/valarray-operators?view=msvc-170#op_star) have different return values\.

Proxy objects can also be used in third\-party libraries, where, of course, their use may vary from version to version\.

For example, proxy objects are used for matrix operations in the [Eigen](https://eigen.tuxfamily.org/index.php?title=Main_Page) library\. The product of two matrices isn't a matrix but a special proxy object called [Eigen::Product](https://eigen.tuxfamily.org/dox/classEigen_1_1Product.html)\. The matrix transpose returns [Eigen::Transpose](https://eigen.tuxfamily.org/dox/classEigen_1_1Transpose.html)\. Many other operations also create proxy objects\. So, if the following was working fine in one version:

```cpp
const auto c = op(a, b);
b = d;
do_something(c);
```

Then it could easily break when you get an update\. What if _op_ now returns a lazy proxy, and you messed up one of the arguments with the next line?

### What to do, and how to fight it

In C\+\+, there's no way\. The only thing you can do is to be careful\. Also, be careful when describing the constraints imposed on types in templates \(preferably as C\+\+20 concepts\)\.

If you're designing a library, think twice about adding implicit proxies to the public API\. If you really want to add them, you may want to consider whether you can do it without the implicit conversions\. A lot of the issues we've covered here arise from implicit conversions\. Maybe it'd be better to make the API a little wordier and less user\-friendly but still secure?

If you're using a library, it may be better to explicitly specify the variable type\. If you want to use _bool_, then specify it\. Do you want a vector element type? Specify _vector<T\>::value\_type_\. The _auto_ keyword is very handy, but only if you know what you're doing\.

#### Useful links

1. Stack Overflow\. [Why isn't vector<bool\> a STL container?](https://stackoverflow.com/questions/17794569/why-isnt-vectorbool-a-stl-container)
1. Vreda Pieterse, Derrick G\. Kourie, Loek Cleophas, Bruce William Watson\. [Performance of C\+\+ bit\-vector implementations](https://www.researchgate.net/publication/220803585_Performance_of_C_bit-vector_implementations)\. 
1. The Eigen documentation\. [Writing efficient matrix product expressions](https://eigen.tuxfamily.org/dox/TopicWritingEfficientProductExpression.html)\.
1. The Eigen documentation\. [Lazy Evaluation and Aliasing](https://eigen.tuxfamily.org/dox/TopicLazyEvaluation.html)\. 

## Errors in object lifetime: use\-after\-move

The move semantics of C\+\+11 are an important and necessary feature that enables you to write higher performance code that doesn't create unnecessary copies, allocations, and deallocations\. This code will also explicitly declare the intention to transfer ownership of a resource from one function to another\. Just like in Rust, which has been a Stack Overflow favorite for many years\. However, there are still some differences\.

An applicant will almost certainly be asked about move semantics in any serious job interview\. A good candidate will somehow explain that, using the example of a vector, one object can take something from another object\. These _&&_'s are just a syntactic workaround, because _const&_ can bind to a temporary object, but we can't change anything below _const_ afterward, while _&_ can't bind to a temporary object, and _by value _has issues with the copy constructor\.\.\. Anyway, things happen\. Eventually, you and the applicant may end up writing a simple _unique\_ptr_ to demonstrate in code how exactly to steal pointers from one object to another\. In theory, this should be enough 99% of the time\.

Meanwhile, in the real world, you come across that intriguing 1%\. We'll discuss those next\.

Even though move semantics is quite efficient in C\+\+, it's still not perfect\. The developers tacked it on as a nice workaround but left a significant issue unresolved\.

Let's take a look at a simple _unique\_ptr_:

```cpp
template<class T>
class UniquePtr {
public:
  explicit UniquePtr(T* raw) : _ptr {raw} {}
  UniquePtr() = default;
  ~UniquePtr() {
       delete _ptr;
  }
  UniquePtr(const UniquePtr&) = delete;
  UniquePtr(UniquePtr&& other) noexcept :
    _ptr { std::exchange(other._ptr, nullptr) } {}
  UniquePtr& operator=(const UniquePtr&) = delete;
  UniquePtr& operator=(UniquePtr&& other) noexcept {
    UniquePtr tmp(std::move(other));
    std::swap(this->_ptr, tmp._ptr);
    return *this;
  } 
private:
  T* _ptr = nullptr;
};

....

UniquePtr<MyType> uptr = ...;
....
// something important is going on with uptr
....
UniquePtr<MyType> b = std::move(uptr);
// nothing stops us from doing 
// uptr = fun(); here
```

As we know, [_std::move_](https://pvs-studio.com/en/blog/terms/6518/) [doesn't move anything](https://medium.com/@dhaneshvb/c-pitfalls-std-move-is-not-moving-anything-c9c073422b83)\. It simply performs a reference conversion to ensure that when a constructor or an assignment statement is called, the correct rvalue\-reference overload is selected\. The original object from which the move was made doesn't go anywhere \(unlike in Rust, where the object becomes unavailable for use after the move\)\. It'll have a destructor called at some point\. So, we need to keep this object in a valid state to call the destructor\. Let's leave _nullptr_ in _UniquePtr_, just like in the move constructor\.

However, what happens in the move assignment operator?

```cpp
UniquePtr& operator=(UniquePtr&& other) noexcept {
  UniquePtr tmp(std::move(other));
  std::swap(this->_ptr, tmp._ptr);
  return *this;
}
```

It uses move\(copy\)\-and\-swap for some reason\.\.\. Well, there's a reason: we probably want to destroy the old object \(_T_, not a pointer\) and take ownership of the new one\. Or do we? If not, why don't we implement the move operator like this?

```cpp
UniquePtr& operator=(UniquePtr&& other) noexcept {
  std::swap(this->_ptr, other._ptr);
  return *this;
}
```

1. Has the data ownership been transferred? It has been\.
1. Is the old pointer object in a valid state to call the destructor? Yes, as good as the one it was assigned to\!

Everything about the semantics of moving in C\+\+ is great\!

However, this behavior is at least unexpected for _UniquePtr_\. So, in the standard implementation, _std::unique\_ptr_ still zeroes the source pointer\. The same is true for _std::shared\_ptr_ and _std::weak\_ptr_\. The standard guarantees that\.\.\.

So, here lies the main trap: while the empty _moved\-out_ state for smart pointers is guaranteed, this actually isn't true for other classes from the standard library \(and not only the standard library\)\! Not true at all\!

### std::vector and other containers similar to it

The behavior of the move operator for a vector is described very intricately and considers a parameter that only those familiar with it—and interested in configuring it—will remember the allocator\.

There's an allocator object hidden in each instance of _std::vector_\. This can be either the default \(_std::allocator_\) empty object that uses global _malloc/operator new_, or something more specific\. For example, you may want each of your vectors to use its own unique, pre\-allocated portion of a large buffer that's completely under your control\.

The standard library asks the allocator type to define the _propagate\_on\_container\_move\_assignment_ property that affects how move assignment behaves\. If we write _A \= std::move\(B\)_, we have three options:

1. _propagate\_on\_container\_move\_assignment\{\} \=\= true_ \(yes, this isn't a constant but a structure, like _false\_type_/_true\_type_\)\. The _A_ vector is deallocated, the allocator is moved \(using the move assignment again, so we need to take care of some guarantees here\), and the whole content is taken from _B_\. _B_ is empty\.
1. _propagate\_on\_container\_move\_assignment\{\} \=\= false_ and the allocator in _A_ and _B_ is the same \(_A\.get\_allocator\(\) \=\= B\.get\_allocator\(\)_\)\. _A_ is deallocated, the allocator stays where it is\. The content is taken from _A_ to _B_\.
1. _propagate\_on\_container\_move\_assignment\{\} \=\= false_ and _A\.get\_allocator\(\) \!\= B\.get\_allocator\(\)_\. This is where the most interesting part begins: _A_ can't take away the allocator or all the data\. The only option is to move each element separately\. However, emptying and deallocating _B_ isn't necessary\. All we need to do is move the elements\. In this case, we can also get a complete vector consisting of moved\-out elements\.

In the libc\+\+ implementation of the vector in the third case, the vector isn't left empty\. The call to _clear\(\)_ is in libstdc\+\+\.

An [example](https://godbolt.org/z/Ya69Ec5GY) shows this:

```cpp
template <class T>
struct MyAlloc {
  using value_type = T;
  using size_type = size_t;
  using difference_type = ptrdiff_t;
  using propagate_on_container_move_assignment = std::false_type;

  T* allocate(size_t n) {
    return static_cast<T*>(malloc(n * sizeof(T)));
  }

  void deallocate(T* ptr, size_t n) {
    free(static_cast<void*>(ptr));
  }


  using is_always_equal = std::false_type;
  bool operator == (const MyAlloc&) const {
    return false;
  }
};

int main() {
  using VectorString = std::vector<std::string, MyAlloc<std::string>>;

  {
    VectorString v = {
      "hello", "world", "my"
    };
    VectorString vv = std::move(v);
    std::cout << v.size() << "\n";
    // outoputs 0. It was a move constructor
  }

  {
    VectorString v = {
      "hello", "world", "my"
    };
    VectorString vv;
    vv = std::move(v);
    std::cout << v.size() << "\n";
    // outputs 3. It was a move assignment
    for (auto& x : v) {
      // every element has been moved, there's nothing here
      std::cout << x;
    }
  }
}
```

Let's compile and run it:

* clang \-std\=c\+\+20 \-stdlib\=libc\+\+: **0 3\.**
* clang \-std\=c\+\+20: **0 0\.**

Note that only move assignment has the issue\! Well, this is also a great example of how breaking the variable declaration and initialization can change the C\+\+ program behavior\!

By the way, strings were the elements of the vector\. And the last loop addresses the moved\-out strings\!

### std::string

Moved\-out string state is also unspecified\.

On various resources dedicated to C\+\+, you may find an example that shows unexpected results when the code is compiled using the old Clang 3\.7 with libc\+\+:

```cpp
void g(std::string v) {
  std::cout << v << std::endl;
}
 
void f() {
  std::string s;
  for (unsigned i = 0; i < 10; ++i) {
    s.append(1, static_cast<char>('0' + i));
    g(std::move(s));
  }
}
```

Since C\+\+11, strings in the implementations of the three major compilers use SSO \(Small String Optimization\)\. With SSO, small strings aren't stored on the heap but within the string object \(instead of/over the _union_ pointers\)\. Copying such strings becomes trivial, and trivial objects \(primitives, structures of primitives\) are also trivially moved by simply copying\. In modern versions of GCC and Clang with libc\+\+ and lidstdc\+\+, the string remains empty after a move operation\. Yet we shouldn't rely on it\.

### What do we do?

There are four levels associated with the moved\-out object state guarantees:

1. **Destructor only**\. The moved\-out object is only good enough to be destroyed and no longer used\. Never\. This is a basic guarantee that one should provide if they decide to add move semantics to their objects, so that the whole destructor autocall mechanism doesn't shoot anyone's feet off;
1. **Destructor & assignment**\. Now we can reuse the object by assigning a new value to it \(and then use it normally\)\. An object that can be moved but can't be reassigned is very rare\. So, usually this guarantee is combined with the previous one;
1. **Valid, but unspecified**\. We can use it and call member functions that don't require preconditions\. What's inside it, though? Hell knows;
1. **Valid, well\-defined**\. It's all clear\.

Read the documentation before reusing an unfamiliar moved\-out object\! Better yet, avoid reusing it at all\. Many static analyzers can issue a warning if you attempt to access a moved\-out object in a function after calling _std::move_ on it\.

Also, when implementing the move operator, use the _move\_and\_swap_ pattern \(as demonstrated with _UniquePtr_ at the beginning\), so you have a better chance of leaving your objects in a truly empty state without much effort\.

#### Useful links

1. SEI CERT C\+\+ Coding Standard\. [EXP63\-CPP\. Do not rely on the value of a moved\-from object](https://wiki.sei.cmu.edu/confluence/display/cplusplus/EXP63-CPP.+Do+not+rely+on+the+value+of+a+moved-from+object)\.
1. Stack Overflow\. Is a moved\-from vector always empty? [Answer](https://stackoverflow.com/questions/17730689/is-a-moved-from-vector-always-empty/17735913#17735913)\. 
1. Cppreference\. [std::allocator](https://en.cppreference.com/w/cpp/memory/allocator)\. 
1. Jonathan Müller\. [Move Safety – Know What Can Be Done in the Moved\-From State](https://www.foonathan.net/2016/07/move-safety/)\.
1. Dhanesh Valappil\. [C\+\+ Pitfalls — std::move is NOT moving anything\!](https://medium.com/@dhaneshvb/c-pitfalls-std-move-is-not-moving-anything-c9c073422b83)
1. Andrey Karpov\. [The code analyzer is wrong\. Long live the analyzer\!](https://pvs-studio.com/en/blog/posts/cpp/0779/)

## Errors in object lifetime: lifetime extension

Extending the lifetime of temporary objects is a broad topic\. It's come up more than once in this series of notes\. After all, the feature works in a fairly limited number of cases, and more often than not you can get a dangling reference\. In this section, however, I want to focus on a less obvious case with not\-so\-expected consequences\.

In C\+\+, the **first time** a temporary object is assigned to a _const lvalue_ or _rvalue_ reference, that object lifetime is extended to the reference lifetime:

```cpp
std::string get_string();
void run(const std::string&);

int main() {
  const std::string& s1 = get_string(); 
  run(s1); // ok, the reference is valid
  std::string&& s2 = get_string();
  run(s2); // ok, the reference is valid
  // but
  std::string&& s3 = std::move(get_string()); // the reference is
                                              // no longer valid!
  // the first assignment — the reference is in
  // the std::move argument, its lifetime is limited by the move body
  // like any other function that accepts
  // and returns the reference (std::move is just an example)
}
```

Here's a slightly less obvious feature: not only a reference to a temporary object has this effect, but [any of its child objects](https://godbolt.org/z/b53j8fbz6)\!

```cpp
#include <iostream>
#include <string>
#include <vector>

struct User {
  std::string name;
  std::vector<int> tokens;
};

User get_user() {
  return {
    "Dmitry",
    {1,2,3,4,5}
  };
}

int main() {
  std::string&& name = get_user().name;
  // some hacky address arithmetics:
  // User is alive, we can access data in it!
  // Build with -fsanitize=address to ensure!
  auto& v = *(std::vector<int>*)((char*)(&name) + sizeof(std::string));
  for (int x : v) {
    std::cout << x;
  }
}
```

The code above outputs the contents of the _tokens_ vector from the _User_ object\. And there's nothing wrong with it: no dangling references or use\-after\-free\. The reference to a data member extends the lifetime of the whole object\. It can be a reference to [any nested data member](https://godbolt.org/z/n3b34f9Mn):

```cpp
struct Name {
  std::string name;
};

struct User {
  Name name;
  std::vector<int> tokens;
};

....

int main() {
  std::string&& name = get_user().name.name;
  ....
}
```

Nested data members can even [exist within arrays](https://godbolt.org/z/9cs3EavfK)\! However, arrays should be of good old C\-style \(_T array\[N\]_\)\. 

```cpp
struct Name {
  std::string name;
};

struct User {
  Name name[2]; 
  std::vector<int> tokens;
};

User get_user() {
  return {
    { "Dmitry", "Dmitry" },
    {1,2,3,4,5}
  };
}

int main() {
  std::string&& name = get_user().name[1].name;
  ...
}
```

<details>
   <summary>A note on std::array\\\.</summary>

This trick [won't work](https://godbolt.org/z/716zW7143) with _std::array_ because of the overloaded _operator \[\]_:

error: rvalue reference to type 'basic\_string<\.\.\.\>' cannot bind to lvalue of type 'basic\_string<\.\.\.\>'

```cpp
23 |     std::string&& name = get_user().name[1].name;
```

Replacing the _std::string&& name_ rvalue reference with _const std::string& name_ helps the code compile and crash with the expected [stack\-use\-after\-free](https://godbolt.org/z/ovrz1Yoo8):

```cpp
....
struct User {
  std::array<Name, 2> name;
  std::vector<int> tokens;
};
....
int main() {
  const std::string& name = get_user().name[1].name;
  std::cout << name << "\n";
}
```

Here's the run result:

```cpp
Program returned: 1
==1==ERROR: AddressSanitizer:
stack-use-after-scope on address0x7e6806200040 at
pc 0x5b1ce93dcf19 bp 0x7ffdc59e7770 sp 0x7ffdc59e7768
READ of size 8 at 0x7e6806200040 thread T0
```


</details>
Great\! However, an inquisitive reader has probably already guessed what the issue is\. We take reference to only one data member, and we're likely to work only with that data member\. The whole object, however, is left to live\.\.\. What if the rest of its data members hold the allocated memory? What if we **really need** them to have a destructor called?

To illustrate the issue, I'll give an example not in C\+\+ but in Rust, since the type that causes issues can be taken from the standard library there, just like a beautifully broken syntactic construct\.

```cpp
use parking_lot::Mutex;

#[derive(Default, Debug)]
struct State {
  value: u64,
}

impl State {
  fn is_even(&self) -> bool {
    self.value % 2 == 0
  }

  fn increment(&mut self) {
    self.value += 1
  }
}

fn main() {
  let s: Mutex<State> = Default::default();

  match s.lock().is_even() {
    true => {
      s.lock().increment(); // oops, double lock!
    }
    false => {
      println!("wasn't even");
    }
  }
  dbg!(&s.lock());
}
```

This [example](https://play.rust-lang.org/?version=stable&mode=release&edition=2021&gist=31f87adf34e0e6c490a46991e3d81a5d) leads to a deadlock: the temporary _LockGuard_ object in the _match_ statement remains alive because of sheer absurdity\! You can learn more about it [here](https://fasterthanli.me/articles/a-rust-match-made-in-hell)\. Now let's get back to C\+\+\.

If, for some reason, we decide to follow Rust's example and explicitly associate the _mutex_ with data \(as it should be 95% of the time\), we get the same [issue](https://godbolt.org/z/9xeerxnh8) with careless reference usage:

```cpp
template <class T>
struct Mutex {
  T data;
  std::mutex _mutex;
    
  explicit Mutex(T data) : data {data} {}
 
  auto lock() {
    struct LockGuard {
    public:
      LockGuard(T& data,
                std::unique_lock<std::mutex>&& guard) :
        data(data), guard(std::move(guard)) {}
      std::reference_wrapper<T> data;
    private: 
      std::unique_lock<std::mutex> guard;
    };

    return LockGuard(this->data, std::unique_lock{_mutex});
  }
};


int main() {
  Mutex<int> m {15};

  // double lock (deadlock, ub) due to LockGuard
  // lifetime extension, remove && and it will be fine
  auto&& data = m.lock().data;
  std::cout << data.get() << "\n";
  auto&& data2 = m.lock().data;
  std::cout << data2.get() << "\n";
}
```

Seasoned C\+\+ advocates may say, "You're your own worst enemy\. Why using a reference when there's a _reference\_wrapper_?" And they'd be right, of course\. Don't worry, though, C\+\+23 now has the same broken construct, just like _match_ in Rust\. This is\.\.\. **range\-based\-for**\!

Most surprisingly, the standard has introduced changes to fix the dangling reference in the construct:

```cpp
for (auto item : get_object().get_container()) { ... }
```

Now they make it possible to get into the exact same deadlock as in Rust:

```cpp
template <class T>
struct Mutex {
  T data;
  std::mutex _mutex;
    
  explicit Mutex(T data) : data {data} {}
 
  auto lock() {
    struct LockGuard {
    public:
      LockGuard(T& data,
                std::unique_lock<std::mutex>&& guard) :
        data(data), guard(std::move(guard)) {}
      std::reference_wrapper<T> data;

      T& get() const {
        return data.get();
      }
      private: 
        std::unique_lock<std::mutex> guard;
    };

    return LockGuard(this->data, std::unique_lock{_mutex});
  }
};

struct User {
  std::vector<int> _tokens;

  std::vector<int> tokens() const {
    return this->_tokens;
  }
};

int main() {
  Mutex<User> m { { {1,2,3, 4,5} } };

  for (auto token: m.lock().get().tokens()) {
    std::cout << token << "\n";
    m.lock(); // deadlock C++23
  }
}
```

The best part about all this is that, currently, this "fixed" behavior hasn't yet been implemented in mainstream compilers\. Soon, however, in about five years, when you update them, many amazing discoveries may await you\!

#### Useful links

1. ISO JTC1/SC22/WG21\. [Wording for P2644R1 Fix for Range\-based for Loop](https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2022/p2718r0.html)\. 
1. Fasterthanlime\. [A Rust match made in hell](https://fasterthanli.me/articles/a-rust-match-made-in-hell)\. 
1. Cppreference\. [Lifetime](https://en.cppreference.com/w/cpp/language/lifetime)\. 

**Author: Dmitry Sviridkin**

Dmitry has over eight years of experience in high\-performance software development in C and C\+\+\. From 2019 to 2021, Dmitry Sviridkin has been teaching Linux system programming at SPbU and C\+\+ hands\-on courses at HSE\.  Currently works on system and embedded development in Rust and C\+\+ for edge servers as a Software Engineer at AWS \(Cloudfront\)\. His main area of interest is software security\. 

**Editor: Andrey Karpov**

Andrey has over 15 years of experience with static code analysis and software quality\. The author of numerous articles on writing high\-quality code in C\+\+\. Andrey Karpov has been honored with the Microsoft MVP award in the Developer Technologies category from 2011 to 2021\. Andrey is a co\-founder of the PVS\-Studio project\. He has long been the company's CTO and was involved in the development of the C\+\+ analyzer core\. Andrey is currently responsible for team management, personnel training, and DevRel activities\.

## All chapters

1. [Part 1](https://pvs-studio.com/en/blog/posts/cpp/1129/): introduction; what is undefined behavior and what it leads to; narrowing conversions and implicit type conversion\.
1. [Part 2](https://pvs-studio.com/en/blog/posts/cpp/1136/): overflow of signed integers; floating\-point numbers; integer promotion; _char_ and sign extension\.
1. [Part 3](https://pvs-studio.com/en/blog/posts/cpp/1149/): dangling references; _string\_view_; a fly in the syntactic sugar \(range\-based for\); self\-reference; _std::vector_ and reference invalidation\.
1. [Part 4](https://pvs-studio.com/en/blog/posts/cpp/1156/): lambda function capture lists; tuples; unexpected mutability; implicit references; use\-after\-move; lifetime extension\.
1. [Part 5](https://pvs-studio.com/en/blog/posts/cpp/1160/): Most Vexing Parse; non\-constant constants; move semantics; _std::enable\_if\_t_ vs\. _std::void\_t_; forgotten _return_\.
1. [Part 6](https://pvs-studio.com/en/blog/posts/cpp/1163/): ellipsis and functions; _operator \[\]_; _iostreams_—good luck debugging\!; comma operator; function\-try\-block; zero\-sized types\.
1. [Part 7](https://pvs-studio.com/en/blog/posts/cpp/1174/): null\-terminated strings; _std::shared\_ptr_; ~~im~~explicit type conversion; how to pass a standard function and not break anything\.
1. [Part 8](https://pvs-studio.com/en/blog/posts/cpp/1178/): infinite loops and halting problem; recursion; false _noexcept_; buffer overflow\.
1. [Part 9](https://pvs-studio.com/en/blog/posts/cpp/1182/): \(N\)RVO vs RAII; null pointer dereferencing; static initialization order fiasco; static inline; ODR violation; reserved names\.
1. [Part 10](https://pvs-studio.com/en/blog/posts/cpp/1193/): trivial types and ABI; uninitialized variables; C\+\+20 unbounded ranges; non\-virtual yet virtual functions; VLA\.
1. [Part 11](https://pvs-studio.com/en/blog/posts/cpp/1199/): invalid pointers; placement new for arrays; data race; mutex deadlock; signal \(un\)safety; how to do everything right and trigger the deadlock\.
1. [Part 12](https://pvs-studio.com/en/blog/posts/cpp/1211/): _std::vector::reserve_ and _std::vector::resize_; unaligned references; time of life and death; static analysis and UB; conclusion\.