1

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
Aamir
  • 1,974
  • 1
  • 14
  • 18
  • 2
    `std::any` might be initialized with `std::optional`... – Jarod42 Aug 19 '23 at 21:30
  • 3
    There's a lot of implicit conversions happening under the hood here. My guess is that your `std::any` is getting initialized with the *whole* optional value, and then the `std::optional` constructor that wraps in a trivial optional (Constructor 8 on [this page](https://en.cppreference.com/w/cpp/utility/optional/optional)) is kicking in. – Silvio Mayolo Aug 19 '23 at 21:32
  • Why do you ever want `std::optional` in the first place? – n. m. could be an AI Aug 20 '23 at 00:26
  • std::any (like std::pair and std::tuple) is more of a helper class for (internal) meta template/library programming. You should consider std::variant or even a type of your own design. I only use those types internally when I can't give data, or types, a name (semantic meaning) because that meaning is not known yet and only will be assigned after my (library) code will be used. – Pepijn Kramer Aug 20 '23 at 06:36

3 Answers3

6

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.

Jarod42
  • 203,559
  • 14
  • 181
  • 302
  • Thanks that makes sense. Do you (or anyone else) know of a way to do this check properly, or is it impossible? I tried recovering the value and checking if that had a value or comparing the value to std::nullopt, but neither of those work. – Daniel Tyebkhan Aug 19 '23 at 21:45
  • Do you want to check that the `optional` stored in `any` is empty? something like: `if (a) { if (auto* o = std::any_cast>(a.get())) { const bool b = o->has_value(); /*..*/ } }`? – Jarod42 Aug 19 '23 at 22:05
  • @Jarod42 unfortunately that requires knowing what specialization of `std::optional` is stored in the `std::any` just to check if it has a value. – Patrick Roberts Aug 19 '23 at 22:09
  • @PatrickRoberts: it is the issue with `std::any`... `std::optional i = 5;` would be an alternative, (so your `std::any` would have `int`/`string`). – Jarod42 Aug 19 '23 at 22:23
  • I'd be interested in knowing if/how you can force the selection of #4 over #8 [here](https://en.cppreference.com/w/cpp/utility/optional/optional)... – Patrick Roberts Aug 19 '23 at 22:27
  • @PatrickRoberts: For #4, *"This constructor does not participate in overload resolution"*, as `std::is_constructible_v&>` is `true`. – Jarod42 Aug 19 '23 at 23:08
  • Oh, thank you, I had misread that bullet, too many "not" and "unless" to keep track of whether the overload participates, and I thought it was participating but that #8 had a higher priority being a forwarding reference. – Patrick Roberts Aug 20 '23 at 04:41
3

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
Ted Lyngmo
  • 93,841
  • 5
  • 60
  • 108
1

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

Red.Wave
  • 2,790
  • 11
  • 17