Search code examples
c++exceptionstd-expected

How to use std::expected in conjunction with std::transform?


I have a flatbuffer. This flatbuffer has a vector<T> that I want to transform using std::transform into a std::vector<U>.

It is possible that T can contain values that are not valid - because they are a plain integer converted to an enum. In this case, I want to abort the whole transform process.

Currently I throw an exception within the lambda of std::transform and catch it in the function that calls the transform function.

std::transform(deserialized_level.rooms.cbegin(), deserialized_level.rooms.cend(), constant_level_data.rooms.begin(),
    [&out_progress, &deserialized_rooms, &deserialized_level](const std::unique_ptr<room_t>& deserialized_room) {
        auto r = room_info{};
        r.position = int3(deserialized_room->position->x, deserialized_room->position->y, deserialized_room->position->z);
        r.sector_width = (unsigned char)(deserialized_room->sector_count_x);
        r.sector_length = (unsigned char)(deserialized_room->sector_count_z);
        auto sector_dimension = sector_size::normal;
        switch(deserialized_room->square_size) {
            case 256:
                sector_dimension = sector_size::smallest;
                break;
            case 512:
                sector_dimension = sector_size::smaller;
                break;
            case 1024:
                sector_dimension = sector_size::normal;
                break;
            case 2048:
                sector_dimension = sector_size::large;
                break;
            case 4096:
                sector_dimension = sector_size::very_large;
                break;
            case 8192:
                sector_dimension = sector_size::huge;
                break;
            case 16384:
                sector_dimension = sector_size::gigantic;
                break;
            default:
                throw std::runtime_error("Unknown sector size detected!" + std::to_string(deserialized_room->square_size));
        }
        //...//
        return r;
});

How would I model this with std::expected? A std::vector<std::expected<T,E>> seems kinda silly.


Solution

  • If the transformation is to be aborted at the first unexpected value, an exception in the lambda is the right choice. It is the only way to cancel a std::transform.

    What you want as a result is probably std::expected<std::vector<U>, std::string>. To do this, you put the std::transform call into a function that returns a corresponding std::expected.

    #include <algorithm>
    #include <expected>
    #include <string>
    #include <vector>
    
    struct room { int size; };
    struct room_info { std::string category;};
    
    std::expected<std::vector<room_info>, std::string>
    info_of(std::vector<room> const& rooms) try {
        std::vector<room_info> infos;
        infos.reserve(rooms.size());
        std::ranges::transform(rooms, std::back_inserter(infos),
            [](room const& r) -> room_info{
                using namespace std::literals;
                switch(r.size) {
                    case 256:
                        return {"smallest"s};
                    case 1024:
                        return {"normal"s};
                    case 4096:
                        return {"very_large"s};
                    case 16384:
                        return {"gigantic"s};
                    default:
                        throw "invalid room size"s;
                }
            });
        return infos;
    } catch (std::string error) {
        return std::unexpected(std::move(error));
    }
    

    Note that you can still throw other exceptions normally. The std::expected here only receives throw's with std::string.

    You can use it like this:

    #include <iostream>
    
    void print(std::expected<std::vector<room_info>, std::string> const& info) {
        if(info) {
            auto const& list = info.value();
            std::cout << "[";
            if(!list.empty()) {
                std::cout << list[0].category;
                for(std::size_t i = 1; i < list.size(); ++i) {
                    std::cout << ", " << list[i].category;
                }
            }
            std::cout << "]\n";
        } else {
            std::cout << info.error() << '\n';
        }
    }
    
    int main() {
        print(info_of({{256}, {16384}, {1024}}));
        print(info_of({{256}, {16385}, {1024}}));
    }
    
    [smallest, gigantic, normal]
    invalid room size