Interaction between std::optional<std::any> and has_value()

200 Views Asked by At

For debugging purposes, I was writing a function which iterates over a vector of optional variables of any type to check which ones were initialized, but the check for has_value() on all of them is returning true, despite no value having ever been assigned to some of them.

I'd appreciate any help pointing out what I'm misunderstanding, as I'm new to C++. The code is below. Note that when the commented line is uncommented, the if statement picks up that the variable had no value.

#include <iostream>
#include <optional>
#include <any>

bool SimpleCheck(std::vector<std::optional<std::any>> toCheck)
{
    bool res = false;
    for (int i = 0; i < toCheck.size(); ++i)
    {
        // toCheck[i] = std::nullopt;
        if (!toCheck[i].has_value())
        {
            std::cout << "item at index " << i << " had no value\n";
            res = true;
        }
    }
    return res;
}

int main() 
{
    std::optional<int> i = 5;
    std::optional<std::string> str;
    std::optional<double> doub = std::nullopt;
    bool check = SimpleCheck({i, str, doub});
    std::cout << check << "\n";
    return 0;
}

My expected output is:

item at index 1 had no value
item at index 2 had no value
1

The actual output is:

0

If the commented line is uncommented, the output is:

item at index 0 had no value
item at index 1 had no value
item at index 2 had no value
1
3

There are 3 best solutions below

7
Jarod42 On BEST ANSWER

With

std::optional<double> doub = std::nullopt;
std::optional<std::any> a = doub; // it is not a copy constructor

a is non empty, but its any is an empty std::optional.

0
Ted Lyngmo On

All std::optional<std::any>s contain a std::any so none are empty. Also, all std::anys contain an std::optional<T> so none of those are empty.

If you want to check if the inner optionals contain values, you need to std::any_cast the std::any to the optional<T> and then check if that has a value:

template <class T>
bool test(const std::any& any) {
    auto o = std::any_cast<T>(&any);
    return o && o->has_value();
}

template <class... Ts>
bool has_optional_value(const std::any& any) {
    return (... || test<std::optional<Ts>>(any));
}

bool SimpleCheck(std::vector<std::optional<std::any>> toCheck) {
    bool res = false;
    for(std::size_t i = 0; i < toCheck.size(); ++i) {
        if (!(toCheck[i].has_value() &&
             toCheck[i].value().has_value() &&
             has_optional_value<int, std::string, double>(toCheck[i].value())))
        {
            std::cout << "item at index " << i << " had no value\n";
            res = true;
        }
    }
    return res;
}

Output:

item at index 1 had no value
item at index 2 had no value
1
0
Red.Wave On

Firstly, optional<any> is not efficient. std::any does have a has_value method too.

Next, std::any is gready and swallows everything fed, so constructing it from std::optional does not check the optional then retrieve the value; it just swallows the optional and contains an optional.

A better approach to your problem would be what I call a python tuple:

#include <any>
#include <array>
#include <ranges>
#include <format>
#include <vector>
#include <optional>

using py_tuple = std::vector<std::any>;

Let's use some C++20,23 sugar:

bool SimpleCheck(py_tuple toCheck)
{
    bool res = false;
    for( auto&& [i/*integer*/, x/*any&*/]
       : toCheck
       | std::views::enumerate )
    {
        if (x.has_value())
            continue;
        std::cout << std::format("item at index {} had no value\n");
        res = true;
    }
    return res;
}

Next we need a transformer:

auto opt_to_any = []<typename T>
(std::optional<T> const& opt) -> std::any
{
    if (opt.has_value() 
        return {opt.value()};//swallow
    else
        return {};//default construct
};

Then we call the test with your inputs:

bool check = SimpleCheck(
             std::array{i, str, doub} 
           | std::views:: transform(opt_to_any)
           | std::ranges::to<std::vector>() );

I was lazy to call opt_to_any to create 3 initializers for py_tuple; so I ended up chaining ranges adapters to create a much longer sequence with same effect (I am dumb too).