MadScientist
MadScientist

Reputation: 3460

Return type deduction: What method is preferred?

I have a class Foo that has two arbitrary data members of type A and B. A call to Foo::operator()(Arg &&) forwards the argument to the two members and returns the sum of the result. I can see several ways to implement all the necessary type deductions. Are there some ways that are preferred and put less strain on the compiler? I mean "strain" in the sense of compile-time, hitting internal limits if you nest it too deep, etc. Can you generalize that or is it highly specific to a given compiler?

I could do the "naive" auto-decltype variant:

template <typename A, typename B>
class Foo
{
public:
  Foo(A a, B b) : m_a(std::move(a)), m_b(std::move(b)) { }

  template <typename Arg>
  auto operator()(Arg && arg) -> decltype(m_a(std::forward<Arg>(arg)) + m_b(std::forward<Arg>(arg)))
  {
    return m_a(std::forward<Arg>(arg)) + m_b(std::forward<Arg>(arg));
  }
private:
  A m_a;
  B m_b;
};

I could write a helper struct that only operates on types and not on "real" instances but on ones created by std::declval<>

template <typename A, typename B, typename Arg>
struct Foo_Returns
{
  typedef decltype(std::declval<A>()(std::declval<Arg>()) +
                   std::declval<B>()(std::declval<Arg>())) type;
}

template <typename A, typename B>
class Foo
{
public:
  Foo(A a, B b) : m_a(std::move(a)), m_b(std::move(b)) { }

  template <typename Arg>
  typename Foo_Returns<A, B, Arg>::type
  operator()(Arg && arg)
  {
    return m_a(std::forward<Arg>(arg)) + m_b(std::forward<Arg>(arg));
  }
private:
  A m_a;
  B m_b;
};

Are there any more possibilities?

Now lets make it harder: We have two traits is_green<> and is _blue<>. If Arg is green, Foo's operator() shall forward Arg to A's and B's member function green and return the sum of the results, analogous if Arg is blue. A type will never be both green and blue. It should be possible to add additional flavors of types (so using a bool value to indicate blue- or green-ness is not allowed).

One variant would use tag dispatching and auto -> decltype(...) whenever possible:

struct green_tag { };
struct blue_tag { };
struct error_tag;

template <typename T>
struct category
{
  typedef typename std::conditional<is_green<T>::value, 
                                    green_tag,
                                    typename std::conditional<is_blue<T>::value,
                                                              blue_tag,
                                                              error_tag
                                                             >::type
                                   >::type type;
}

template <typename A, typename B>
class Foo
{
public:
  Foo(A a, B b) : m_a(std::move(a)), m_b(std::move(b)) { }

  template <typename Arg>
  auto operator()(Arg && arg) -> decltype(impl(std::forward<Arg>(arg), typename category<Arg>::type()))
  {
    return impl(std::forward<Arg>(arg), typename category<Arg>::type());
  }

private:
  template <typename Arg>
  auto impl(Arg && arg, green_tag) -> decltype(m_a.green(std::forward<Arg>(arg)) + m_b.green(std::forward<Arg>(arg)))
  {
    return m_a.green(std::forward<Arg>(arg)) + m_b.green(std::forward<Arg>(arg));
  }

  template <typename Arg>
  auto impl(Arg && arg, blue_tag) -> decltype(m_a.blue(std::forward<Arg>(arg)) + m_b.blue(std::forward<Arg>(arg)))
  {
    return m_a.blue(std::forward<Arg>(arg)) + m_b.blue(std::forward<Arg>(arg));
  }

  A m_a;
  B m_b;
};

Another version could use helper structs:

template <typename A, typename B, typename Arg, typename Category = typename category<Arg>::type>
struct Foo_Returns;

template <typename A, typename B, typename Arg>
struct Foo_Returns<A, B, Arg, green_tag>
{
  typedef decltype(std::declval<A>().green(std::declval<Arg>()) +
                   std::declval<B>().green(std::declval<Arg>())) type;

  type operator()(A & a, B & b, Arg && arg) const
  {
    return a.green(std::forward<Arg>(arg)) + b.green(std::forward<Arg>(arg));
  }  
};

template <typename A, typename B, typename Arg>
struct Foo_Returns<A, B, Arg, blue_tag>
{
  typedef decltype(std::declval<A>().blue(std::declval<Arg>()) +
                   std::declval<B>().blue(std::declval<Arg>())) type;

  type operator()(A & a, B & b, Arg && arg) const
  {
    return a.blue(std::forward<Arg>(arg)) + b.blue(std::forward<Arg>(arg));
  }  
};

template <typename A, typename B>
class Foo
{
public:
  Foo(A a, B b) : m_a(std::move(a)), m_b(std::move(b)) { }

  template <typename Arg>
  typename Foo_Returns<A, B, Arg>::type
  operator()(Arg && arg)
  {
    return Foo_Returns<A, B, Arg>()(m_a, m_b, std::forward<Arg>(arg));
  }

private:
  A m_a;
  B m_b;
};

Is any version better? What other methods are possible?

Upvotes: 1

Views: 188

Answers (1)

Daniel Frey
Daniel Frey

Reputation: 56873

I would avoid all helper classes/structs. Each helper requires the compiler to store it somewhere and to do additional lookups. Without the classes, the compiler gets at least a chance to optimize things, but I can't imagine that a helper class can improve the situation in the examples you showed.

For your green/blue example, I'd even consider SFINAE to keep the code shorter and the number of instantiated classes/methods lower:

template <typename A, typename B>
class Foo
{
public:
  Foo(A a, B b) : m_a(std::move(a)), m_b(std::move(b)) { }

  template <typename Arg>
  auto operator()(const Arg & arg) ->
    typename std::enable_if< is_green<Arg>::value,
      decltype(m_a.green(arg) + m_b.green(arg) >::type
  {
    return m_a.green(arg) + m_b.green(arg);
  }

  template <typename Arg>
  auto operator()(const Arg & arg) ->
    typename std::enable_if< is_blue<Arg>::value,
      decltype(m_a.blue(arg) + m_b.blue(arg)) >::type
  {
    return m_a.blue(arg) + m_b.blue(arg);
  }

private:
  A m_a;
  B m_b;
};

I'd also imagine this to be more maintainable, YMMV. For the compile-time performance, there is, as always, only one real advice: Measure it.

EDIT: I changed the parameter from Arg&& to const Arg& and dropped the double std::forward<Arg>(...) as this is illegal, see comment.

Upvotes: 0

Related Questions