Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

C++17 structured binding that also includes an existing variable

This SO answer lists some shortcomings of C++17 decomposition declarations (the feature formerly known as "structured binding"). For example, you can't give explicit types to the new variables, and so on. But one big shortcoming I'm running into isn't mentioned there, so I wonder if there's a known workaround that I'm just not thinking of.

Consider this JSON-parsing code (which may contain other bugs; please ignore them for the purposes of this question):

using Value = std::any;
using String = std::string;
using Object = std::map<String, Value>;

std::pair<String, const char *> load_string(const char *p, const char *end);
std::pair<Value, const char *> load_value(const char *p, const char *end);
const char *skip_spaces(const char *p, const char *end);

std::pair<Object, const char *> load_object(const char *p, const char *end)
{
    p = skip_spaces(p, end);
    if (p == end || *p++ != '{') throw ParseError("Expected {");
    p = skip_spaces(p, end);
    Object result;
    if (p == end && *p == '}') {
        // the object has no key-value pairs at all
    } else {
        while (true) {
            auto [key, p] = load_string(p, end);
            p = skip_spaces(p, end);
            if (p == end || *p++ != ':') throw ParseError("Expected :");
            auto [value, p] = load_value(p, end);
            result.insert_or_assign(std::move(key), std::move(value));
            p = skip_spaces(p, end);
            if (p == end) throw ParseError("Expected , or }");
            if (*p == '}') break;
            if (*p++ != ',') throw ParseError("Expected , or }");
        }
    }
    return {result, p+1};
}

This would work great, except that the lines starting auto [key, p] = and auto [value, p] = are invalid! The variable p has already been declared. I'm trying to assign p a new value, but I don't want to create a whole new local variable.

I would prefer not to use std::tie(key, p) =, because that requires me to give a declaration for key before the assignment. This is the familiar old objection to std::tie. Which I could have sworn was the reason structured binding was introduced into the language!

So is there any workaround — any nice clean way of writing the combination construct-key-in-place-and-also-assign-to-p that expresses my intention?

It's strange how I never missed this feature before, but as soon as you give me structured binding to play with, the first thing I try doesn't work. :(

like image 474
Quuxplusone Avatar asked Mar 04 '17 00:03

Quuxplusone


3 Answers

In cases with more complex types, a simple workaround of a movable temporary new object might be the simplest desirable step in the direction of what you want (though I reckon in your particular case it might be simpler to stick to traditional tie instead):

... // (as in your code: p & end exist already, key & p_ not yet)

auto [key, p_] = load_string(p, end);
p = move(p_);

... // (continue using p)

I'm sorry I eventually couldn't compile it myself though I could see it being a problem of my IDE (CLion which currently only halfly supports C++17) though I'd expect it to work in general.

like image 106
FlorianH Avatar answered Nov 16 '22 16:11

FlorianH


#include <iostream>
#include <limits>
#include <tuple>

int main()
{
    auto step = std::numeric_limits<double>::infinity();
    auto as = std::numeric_limits<int>::infinity();

    std::tie(step, as) = std::tuple{ 0.1, 2 };
    std::cout << step << ", " << as << std::endl;
}
like image 21
Catriel Avatar answered Nov 16 '22 18:11

Catriel


This is a really dumb idea that I wouldn't seriously suggest unless there ends up being no sane workaround... but consider the following code.

template<size_t P, size_t... Is>
auto plus(std::index_sequence<Is...>)
{
    return std::index_sequence<P+Is...>{};
}

template<typename RHS, size_t... Is>
auto tuple_select(RHS&& rhs, std::index_sequence<Is...>)
{
    return std::forward_as_tuple(std::get<Is>(std::forward<RHS>(rhs))...);
}

template<typename... Ts>
struct AndTie {
    std::tuple<Ts&...> v;
    AndTie(Ts&... vs) : v(vs...) {}

    template<typename RHS>
    auto operator=(RHS&& rhs) && {
        constexpr int N = std::tuple_size_v<RHS>;
        constexpr int K = sizeof...(Ts);
        v = tuple_select(std::forward<RHS>(rhs), plus<N-K>(std::make_index_sequence<K>{}));
        return tuple_select(std::forward<RHS>(rhs), std::make_index_sequence<N-K>{});
    }
};

This gives us

auto [key] =AndTie(p)= load_string(p, end);
auto [value] =AndTie(p)= load_value(p, end);

It still has the limitation that the "tied" lvalues are constrained to appear last and the "declared" variables are constrained to appear first, but I don't think there's much way to get around that. And something like tuple_shuffle<Is...> could handle that if you needed it.

like image 2
Quuxplusone Avatar answered Nov 16 '22 18:11

Quuxplusone