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

2

8

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?

Secretin answered 29/10, 2015 at 17:52 Comment(5)
I'm just curious. Why are you interested in doing this? What problem are you solving?Dao
I'm implementing something similar to a static_for that executes a callable object over heterogeneous values with an user-specified arity, and also allows the user to retrieve the current iteration number at compile time and break/continue (exit early) at compile time using static_if. Part of the implementation requires passing the first N arguments of the variadic argument pack to another inner function, and I was trying to generalize that by defining some variadic argument pack manipulation functions. Other than nth<I>, I require subpack<I, J> to fully generalize that behaviorSecretin
By user-specified arity, I mean that the static_for iterates over the heterogeneous values in groups of N (where N is a template parameter specified by the user). The callable object that represents the body of the static_for needs to have an operator() with the same arity. Also, I'm doing all of this for an open-source C++14 general purpose library (vrm_core), written for fun and learning purposes. Hope that answers your question :)Secretin
Do not use decltype(xs). The type pack Ts... is exactly what you need. So try std::tuple<Ts...>Flathead
@AndreyNasonov: works perfectly, not sure how I didn't think about that. Thanks! Could you post this solution again as an answer so that I can accept it?Secretin
F
10

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.

Flathead answered 29/10, 2015 at 18:38 Comment(0)
I
3

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)...`
Iaria answered 3/3, 2017 at 20:8 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.