1
votes

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?

1
gcc-4.8 with -std=c++1y and just auto as return type. ;) - Stephan Dollberg
i havent read all you wrote. but when you use the members, it also works when your member function is const (the member will be regarded as const). also then you need to declare the members before the function. as in your code, it wouldnt work. - Johannes Schaub - litb

1 Answers

0
votes

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.