c++c++17c++20stdoptionalranged-loops

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(): std::optional::value() from cppreference.com

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>).


Solution

  • 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.