Wouter van Ooijen
Wouter van Ooijen

Reputation: 1039

How to store a reference when it is an lvalue, store a copy when it is an rvalue

I want to make a factory function template that can be called with a fixed number of parameters, each parameter type a template parameter. With two parameters :

template< typename T1, typename T2 >
and_implementation< T1, T2 > and( T1 && p1, T2 && p2 ){
   return and_implementation< T1, T2 >( p1, p2 );
}

In the and_implementation object I want to store a reference to each parameter that is an lvalue, and a copy of each parameter that is an rvalue. I don't want to use the heap.

The goal is that when I write

auto p1 = ....
auto p2 = ....
auto p3 = and( p1, p3 );

the p3 object contains only references to p1 and p2, but when I write something like

auto p1 = ....
auto p2 = ....
auto p3 = ....
auto p4 = and( p1, and( p2, p3 ));   

the p4 object contains a referencde to p1, but a copy of and(p2, p3).

Is there a way to do this?

What I came up with (the factory is called invert and has only one parameter) is

template< typename T >
struct invert_impl: public gpio {

   T pin;

   template< typename TT > invert_impl( TT && p ):  
      pin( p ) {} // this is line 60

};

template< typename P >
invert_impl< P > invert( P && pin ){
    return invert_impl< P >( pin );
}

This works for

autp pin4 = lpc_gpio< 4 >{};
auto led = invert( pin4 );

but for

autp pin4 = lpc_gpio< 4 >{};
auto led = invert( invert( pin4 ));

I get (GCC 4.9.3):

main.cpp:60:14: error: invalid initialization of reference of type 'lpc_gpio<4>&' from expression of type 'invert_impl<lpc_gpio<4>&>'

Upvotes: 1

Views: 258

Answers (2)

Yakk - Adam Nevraumont
Yakk - Adam Nevraumont

Reputation: 275385

Just because it is slick:

template<template<class...>class Z, class...Ts>
Z<Ts...> make( Ts&&... ts ) {
  return {std::forward<Ts>(ts)...};
}

is a function that can be called like: make<invert_impl>( pin ), and it deduces the type arguments for invert_impl. Now, the downside is that it has a bad name. So we can use a function object:

template<template<class...>class Z,class=void> // =void for SFINAE
struct make {
  template<class...Ts>
  Z<Ts...> operator()(Ts&&...ts)const{
    return {std::forward<Ts>(ts)...};
  }
};

and now we can do:

static make<invert_impl> invert;

and invert(blah) does the right thing (tm), as does

static make<and_implementation> _and_;

without having to rewrite the glue code. (Note: a variable or function named and is actually illegal, because under C++ and is an alias for && -- so I called mine _and_).

Now this gets more fun when we add in named operators (bwahaha).

First the dozen-line library:

namespace named_operator {
  template<class D>struct make_operator{};

  template<class T, char, class O> struct half_apply { T&& lhs; };

  template<class Lhs, class Op>
  half_apply<Lhs, '*', Op> operator*( Lhs&& lhs, make_operator<Op> ) {
    return {std::forward<Lhs>(lhs)};
  }
  template<class Lhs, class Op, class Rhs>
  auto operator*( half_apply<Lhs, '*', Op>&& lhs, Rhs&& rhs )
  -> decltype( invoke( std::forward<Lhs>(lhs.lhs), Op{}, std::forward<Rhs>(rhs) ) )
  {
    return invoke( std::forward<Lhs>(lhs.lhs), Op{}, std::forward<Rhs>(rhs) );
  }
}    

then what I think is an implementation:

template<template<class...>class,class...Ts>
std::false_type is_unary(Ts...) { return {}; }
template<template<class>class> std::true_type is_unary() { return {}; }

template<template<class...>class Z>
using unary = decltype( is_unary<Z>() );

template<template<class...>class Z>
struct make<
  Z,std::enable_if_t<!unary<Z>{}>
>:named_operator::make_operator<make<Z>> {
  template<class...Ts>
  Z<Ts...> operator()(Ts&&...ts)const{
    return {std::forward<Ts>(ts)...};
  }
  template<class Lhs, class Rhs>
  friend Z<Lhs, Rhs> invoke( Lhs&& lhs, make<Z> m, Rhs&& rhs ) {
    return m(std::forward<Lhs>(lhs), std::forward<Rhs>(rhs));
  }
};

which gives us

auto r = p1 *_and_* p2;

as an alternative to

auto r = _and_(p1, p2);

which is just fun. (assuming I dotted all the is and crossed all the ts above)

Upvotes: 1

user743382
user743382

Reputation:

You're over-thinking things. Your constructor doesn't need to be a template, since in every concrete template instantiation, you already know the exact type your constructor should accept: it should accept a T.

template <typename T>
struct invert_impl : public gpio {
  T pin;
  invert_impl(T p) : pin(p) {}
};

The reason your template constructor is failing is because it also gets selected as a copy- or move-constructor (if it's a better match than the implicitly generated copy- and move constructors), which cannot work. The copy- and move constructor take const invert_impl & and invert_impl &&, which cannot be used to initialise pin.

Note: the initialisation of pin from p may be making an unnecessary copy here. std::forward can avoid that, even though this isn't exactly what it's originally intended for.

  invert_impl(T p) : pin(std::forward<T>(p)) {}

@Yakk rightly points out that there are still some unnecessary operations even then, and they can be avoided by making the constructor take T&& instead, and forwarding from invert, like so:

template <typename T>
struct invert_impl : public gpio {
  T pin;
  invert_impl(T &&p) : pin(std::forward<T>(p)) {}
};

template <typename T>
invert_impl<T> invert(T &&pin) {
  return invert_impl<T>(std::forward<T>(pin));
}

Upvotes: 1

Related Questions