Adrian
Adrian

Reputation: 10911

Is there a way to convert a list of lvalues and rvalues to a tuple with reference types and full types respectively?

So given the following 2 functions:

int rvalue();
int& lvalue();

The following would be valid:

std::tuple<int&, int> x = appropriate_fn(lvalue(), rvalue());

I was thinking something like it like this:

template <typename T, typename...Ts>
auto make_comparible(T const& arg, Ts&&...args)
{
    return std::make_tuple(T(arg), make_comparible(args...));
}

template <typename T, typename...Ts>
auto make_comparible(T& arg, Ts&&...args)
{
    return std::make_tuple<T&>(arg, make_comparible(args...));
}

template <typename T>
auto make_comparible(T const& arg)
{
    return std::tuple<T>(T(arg));
}

template <typename T>
auto make_comparible(T& arg)
{
    return std::tuple<T&>(arg);
}

But there's three issues this that I can see.

  1. This is not a simple std::tuple, but a nested one. Which, come to think about it, may not be an issue as I just want to do comparisons (less than, equal to) on it and should still work.

  2. This doesn't distinguish between a temporary and a const reference. This is a bit annoying but I don't see any way around it.

  3. Most importantly, it doesn't work. Given the following:

    std::tuple<int, std::tuple<int&>> x = make_comparible(rvalue(), lvalue());
    std::tuple<int&, std::tuple<int>> y = make_comparible(lvalue(), rvalue());
    

    The first one is works, but the second one give an error because make_comparible() is returning std::tuple<int, std::tuple<int&>> instead of std::tuple<int&, std::tuple<int>>. Demo

So, is what I'm asking for possible, or is it a pipe dream?

The use case is that I want to define a function in a class that will return a tuple (or otherwise comparable type), which will not result in a dangling pointer/reference, and which will be easy to use.

EDIT

Ok, so after watching C++ and Beyond 2012: Scott Meyers - Universal References in C++11, it looks like overloading on a universal reference is almost always an error. But as I am trying to differentiate a lvalue from an rvalue, irrespective of constness, this would be the right way to go.

I can use overloading to get the universal reference to bind to only rvalues, if I declare overloads which are for lvalues. I had also forgotten to use std::forward() which was a brain fart on my part. I should have known better.

#include <iostream>
#include <tuple>

// forward declarations
template <typename T, typename...Ts>
decltype(auto) make_comparible(T const& arg, Ts&&...args);
template <typename T, typename...Ts>
decltype(auto) make_comparible(T& arg, Ts&&...args);
template <typename T>
decltype(auto) make_comparible(T&& arg);
template <typename T>
decltype(auto) make_comparible(T const& arg);
template <typename T>
decltype(auto) make_comparible(T& arg);

// rvalue
template <typename T, typename...Ts>
decltype(auto) make_comparible(T&& arg, Ts&&...args)
{
    std::cout << "rvalue ";
    // want to copy, so do not use std::move()
    return std::make_tuple(arg, make_comparible(std::forward<Ts>(args)...));
}

// lvalue const
template <typename T, typename...Ts>
decltype(auto) make_comparible(T const& arg, Ts&&...args)
{
    std::cout << "lvalue const ref ";
    // This is a reference, so store as a reference
    return std::make_tuple<T const&>(arg, make_comparible(std::forward<Ts>(args)...));
}

// lvalue
template <typename T, typename...Ts>
decltype(auto) make_comparible(T& arg, Ts&&...args)
{
    std::cout << "lvalue ref ";
    // This is a reference, so store as a reference
    return std::make_tuple<T&>(arg, make_comparible(std::forward<Ts>(args)...));
}

// rvalue
template <typename T>
decltype(auto) make_comparible(T&& arg)
{
    std::cout << "rvalue ";
    // want to copy, so do not use std::move()
    return std::tuple<T>(arg);
}

// lvalue const
template <typename T>
decltype(auto) make_comparible(T const& arg)
{
    std::cout << "lvalue const ref ";
    // This is a reference, so store as a reference
    return std::tuple<T const&>(arg);
}

// lvalue
template <typename T>
decltype(auto) make_comparible(T& arg)
{
    std::cout << "lvalue ref ";
    // This is a reference, so store as a reference
    return std::tuple<T&>(arg);
}

int var = 5;
int rvalue() { return 4; }
int& lvalue() { return var; }
int const& const_lvalue() { return var; }

int main()
{
    // expect output "rvalue lvalue ref", OK
    std::tuple<int, std::tuple<int&>> x = make_comparible(rvalue(), lvalue());
    std::cout << std::endl;

    // expect output "rvalue lvalue const ref", OK
    std::tuple<int, std::tuple<int const&>> y = make_comparible(rvalue(), const_lvalue());
    std::cout << std::endl;

    // expect output "lvalue ref lvalue const ref rvalue", OK
    make_comparible(lvalue(), const_lvalue(), rvalue());
    // But this doesn't work.  Type returned was std::tuple<int, std::tuple<int, std::tuple<int> > >. WHY?
    std::tuple<int&, std::tuple<int const&, std::tuple<int>>> z = make_comparible(lvalue(), const_lvalue(), rvalue());
    std::cout << std::endl;

    return 0;
}

So the code path is correct. But the type returned is wrong. I'm getting a std::tuple<int, std::tuple<int, std::tuple<int>>> instead of a std::tuple<int&, std::tuple<int const&, std::tuple<int>>>. WHY?

Upvotes: 3

Views: 289

Answers (2)

darkdragon
darkdragon

Reputation: 460

Based on @max66's answer, I created a class which stores constructor arguments as a tuple member variable (lvalues as references and rvalues are moved):

#include <type_traits>
#include <tuple>
#include <functional>
#include <utility>

int rVal() { return 0; }
int& lVal() { static int val{1}; return val; }

template<typename T>
struct add_reference_wrapper{ using type = T; };
template<typename T>
struct add_reference_wrapper<T&> : public add_reference_wrapper<std::reference_wrapper<T>>{};
template<typename T>
using add_reference_wrapper_t = typename add_reference_wrapper<T>::type;

template<typename T>
struct remove_reference_wrapper{ using type = T; };
template<typename T>
struct remove_reference_wrapper<std::reference_wrapper<T>> : public remove_reference_wrapper<T&>{};
template<typename T>
using remove_reference_wrapper_t = typename remove_reference_wrapper<T>::type;

template<typename... Args>
struct S{
    S(Args&&... args_) : args{std::forward<Args>(args_)...} {}
    std::tuple<remove_reference_wrapper_t<Args>...> args;
};
template<typename... Args>
S(Args&&...) -> S<add_reference_wrapper_t<Args>...>;

int main() {
    S s{rVal(), lVal()};
    static_assert(std::is_same_v<decltype(s.args), std::tuple<int, int&>>);
}

Upvotes: 0

max66
max66

Reputation: 66230

If I understand correctly what do you want, you can use std::reference (to wrap a l-value reference so that std::make_tuple() produce std::tuple with a reference in the corresponding position), and std::forward, to get the correct type of reference from a variadic list of arguments.

So you can write a couple of convert functions

int rVal ()
 { return 0; }

int & lVal ()
 { static int val { 1 }; return val; }

and make_comparable() become

template <typename ... Ts>
auto make_comparible (Ts && ... args)
 { return std::make_tuple(convert(std::forward<Ts>(args))...); }

if you can use C++14/C++17 (auto return type), or

template <typename ... Ts>
auto make_comparible (Ts && ... args)
   -> decltype(std::make_tuple(convert(std::forward<Ts>(args))...))
 { return std::make_tuple(convert(std::forward<Ts>(args))...); }

or also (simpler)

template <typename ... Ts>
auto make_comparible (Ts && ... args)
   -> decltype(std::make_tuple(convert(std::forward<Ts>(args))...))
 { return { convert(std::forward<Ts>(args))... }; }

if you must use C++11 (auto plus decltype(); ugly but works).

The following is a full working (C++14) example.

#include <tuple>
#include <functional>

int rVal ()
 { return 0; }

int & lVal ()
 { static int val { 1 }; return val; }

template <typename T>
std::reference_wrapper<T> convert (T & t)
 { return t; }

template <typename T>
T convert (T && t)
 { return std::move(t); }

template <typename ... Ts>
auto make_comparible (Ts && ... args)
 { return std::make_tuple(convert(std::forward<Ts>(args))...); }

int main ()
 {
   auto t = make_comparible(rVal(), lVal());

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

Upvotes: 2

Related Questions