range-based for-loop in C++ over std::optional<Container> does not work

range-based for-loop in C++ over std::optional<Container> does not work

Let me start with a C++ code that simplifies my issues I faced in the actual code base. I compiled it with --std=c++20 and --std=c++17. The first for-loop below was okay; the second for-loop, which returns std::optional<Container> was not, for all of the multiple containers I have tried. I'd like to understand why:

#include <iostream>
#include <optional>
#include <string>
#include <unordered_set>

std::unordered_set<std::string> GenerateSet() {                                                                                                                                                                                                      
  std::unordered_set<std::string> names = {"a", "b"};                                                                                                                                                                                                
  return names;                                                                                                                                                                                                                                      
}

std::optional<std::unordered_set<std::string>> GenerateOptionalSet() {                                                                                                                                                                               
  std::unordered_set<std::string> names = {"a", "b"};                                                                                                                                                                                                
  return names;                                                                                                                                                                                                                                      
}

int main() {                                                                                                                                                                                                                                         
  std::cout << "When a set is returned: {";                                                                                                                                                                                                          
  for (const auto& e : GenerateSet()) {                                                                                                                                                                                                              
    std::cout << e << " ";                                                                                                                                                                                                                           
  }                                                                                                                                                                                                                                                  
  std::cout << "}" << std::endl;                                                                                                                                                                                                                     
                                                                                                                                                                                                                                                     
  std::cout << "When a optional of a set is returned: {";                                                                                                                                                                                            
  for (const auto& e : GenerateOptionalSet().value()) {                                                                                                                                                                                              
    std::cout << e << " ";                                                                                                                                                                                                                           
  }                                                                                                                                                                                                                                                  
  std::cout << "}" << std::endl;                                                                                                                                                                                                                     
                                                                                                                                                                                                                                                     
  return 0;                                                                                                                                                                                                                                          
}

The result was segmentation faults at runtime (fairly recent clang) or no iteration at all in the second for-loop (fairly old gcc on an archaic Linux box).

Here's the URL I referred to regarding the std::optional<T>::value(): Read more

There seem to be 4 different versions. I was not quite sure which version of the 4 overridden functions would be invoked and why it does not work as I expected (i.e. just looping over the value of the returned, temporary std::optional<T>).

ChatGPT insisted that both loops in the code work. I tried to search stackoverflow, etc, and couldn't find the right answer to the particular problem I have.

Answer

The issue here is what your reference gets bound to. In C++20 the right hand side of the : gets bound to an auto&& variable so for the first loop you have

auto&& range_ref = GenerateSet();

and this is okay since GenerateSet() returns an rvalue std::unordered_set<std::string> and range_ref extends the lifetime of the returned rvalue.

With your second loop you get

auto&& range_ref = GenerateOptionalSet().value();

which is an rvalue that calls a function that yields an lvalue since value() returns by reference. Because of this there is no temporary lifetime extension and your reference is now a dangling reference. Any access of the reference has undefined behavior and any results you get are correct.


This has been addresses in C++23 with P2644 which will extend the lifetime of the intermediate rvalue object.

Enjoyed this article?

Check out more content on our blog or follow us on social media.

Browse more articles