Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Remove rvalueness, keep lvalue references (standard type trait available?)

I'm trying to write a function that returns a subset of a variadic argument pack under the form of an std::tuple. The function should ideally have no runtime overhead (no unnecessary copies), and it should allow users to access lvalue references and modify them.

Value types, lvalue references and const lvalue references should be maintained. Temporaries (rvalue references), should be "converted" to value types to avoid creating invalid references (references to temporaries).

Example of desired results:

int lr = 5;
const int& clr = lr;

auto t = make_subpack_tuple(lr, clr, 5);

static_assert(is_same
<
    decltype(t), 
    std::tuple<int&, const int&, int>
>{}, "");

// Ok, modifies lr:
std::get<0>(t) = 10;

// Compile-time error, intended:
// std::get<1>(t) = 20;

// Ok, 5 was moved into the tuple:
std::get<2>(t) = 30;

Example incomplete implementation:

template<typename... Ts>
auto make_subpack_tuple(Ts&&... xs)
{
    return std::tuple
    <
        some_type_trait<decltype(xs)>...
    >
    (
        std::forward<decltype(xs)>(xs)...
    );
}

Does what I am trying to do make sense?

Is there a standard type-trait that can be used in place of some_type_trait? Or should I implement my own solution?

like image 207
Vittorio Romeo Avatar asked Oct 29 '15 17:10

Vittorio Romeo


2 Answers

The solution for you will be

template<typename... Ts>
auto make_subpack_tuple(Ts&&... xs)
{
    return std::tuple<Ts...>(std::forward<Ts>(xs)...);
}

According to template argument deduction rules, the parameter pack Ts... will contain only cv-qualified types and lvalues. The information in this question may be useful too.

like image 81
Andrey Nasonov Avatar answered Sep 24 '22 00:09

Andrey Nasonov


I'd just like to chime in that I ran into this same not-really-a-problem ("I think I need to decay rvalue references and keep lvalue references untouched") while implementing an efficient version of Nick Athanasios's foldable Op<operation>. I had had this mess:

template<class Pack, class Op>
struct Foldable
{
    mystery_trait_t<Pack> value;
    const Op& op;

    template<class RhsPack>
    auto operator*(const Foldable<RhsPack, Op>& rhs) const {
        return op(static_cast<std::decay_t<Pack>>(
            (op.f)(std::move(value), std::move(rhs.value))
        ));
    }

    operator mystery_trait_t<Pack> () && {
        return std::move(value);
    }
};

template<class Pack>
auto NamedOperator::operator()(Pack&& value) const {
    return Foldable<Pack, NamedOperator>(std::forward<Pack>(value), *this);
}

and (after puzzling for a bit, and then starting to ask a SO question, and finding this existing question/answer, and adding a static_assert to my implementation of mystery_trait_t to verify that it was never actually invoked with an rvalue reference type!) it turned out that all I actually needed was

template<class Pack, class Op>
struct Foldable
{
    Pack value;
    const Op& op;

    template<class RhsPack>
    auto operator*(const Foldable<RhsPack, Op>& rhs) const {
        return op(
            (op.f)(std::move(value), std::move(rhs.value))
        );
    }

    operator Pack () && {
        return std::move(value);
    }
};

(See my whole code on Wandbox.)

This "answer" of mine doesn't contribute any new information, but I thought it would be useful to share, because it just goes to show that even if you think you're way deep in template metaprogramming and are sure you need this "conditional decay" behavior... you really don't need it!

There might be a corollary general rule that writing any_template<T&&> is always a code smell. In Vittorio's original question, he effectively did that twice, although both times it was hidden by decltype syntax:

some_type_trait<decltype(xs)>...  // should have been `Ts...`
std::forward<decltype(xs)>(xs)... // could equally well be `std::forward<Ts>(xs)...`
like image 25
Quuxplusone Avatar answered Sep 26 '22 00:09

Quuxplusone