1

Given this code

struct data {
    int velocity;
};

template <typename Data>
class Collector {
  // ...

public:
    void add(const Data& data) {}

    template <typename T>
    T average1(T Data::*field) const {
        return T{}; // Some calculation here
    }

    template <T Data::*field>
    T average2() const {
        return T{}; // Some calculation here
    }
};

void foo() {
    Collector<data> collector;

    // I have no problem handling the average by sending member as parameter

    auto ok = collector.average1(&data::velocity);

    // But compilation here fails
    auto error = collector.average2<&data::velocity>();
}

My intention is to replace passing pointers to members to functions by template arguments, but could not match simultaneously member type and member, I can do something like

template <typename T, T Data::*field>
T average2() const {
    return T{}; // Some calculation here
}

but then I have to invoke as

auto error = collector.average2<int, &data::velocity>();

and that is a but ugly and seems unnecessary

Do you have an idea on how to fix this or a better approach to collect this kind of data?

Thanks in advance

1 Answers1

4

In C++17, you can make the templated version work by broadening the value template parameter to auto and resolve T later down the line, with a decltype() for example.

#include <type_traits>

template <typename Data>
class Collector {
  // ...

  public:
    template <auto field>
    auto average2() const {
      using T = decltype(std::declval<Data>().*field);
      return T{}; // Some calculation here
    }
};

void foo() {
    Collector<data> collector;

    // Works perfectly fine
    auto error = collector.average2<&data::velocity>();
}

In C++20, you can make this cleaner still by constraining field to Data member pointers. This will give you tighter overload resolution as well as nicer error messages.

#include <type_traits>

template<typename PtrT, typename ObjT>
concept MemberObjectPointerFor = std::is_member_object_pointer_v<PtrT> &&
  requires(PtrT ptr, ObjT& obj) {
    { obj.*ptr };
  };

template <typename Data>
class Collector {
  // ...

public:
    template <MemberObjectPointerFor<Data> auto field>
    auto average2() const {
        using T = decltype(std::declval<Data>().*field);
        return T{}; // Some calculation here
    } 
};
  • All that being said, I would personally just stick to `average1()`. This is something compilers can trivially optimize away most of the time. –  Oct 29 '21 at 14:30
  • I think having a template parameter instead of a placeholder function arg is more meaningful and intuitive when reading the code. It is however very c++20 dependent, so having that approach available is valuable. OP didn't tag a c++ version, so it is legit, but at this time still limiting. To that point. This is very bleeding edge. Although this works fine in clang (trunk), gcc (trunk) currently hits an internal error on this approach: https://godbolt.org/z/jn8n3Y9qr [date of issue: 2021/10/29] Known issue -- or should I file? – Glenn Teitelbaum Oct 29 '21 at 15:50
  • @GlennTeitelbaum The concept is not strictly necessary for this to work, it only cleans things up a bit more. The basic `auto` version is C++17, which is not really bleeding-edge anymore. The gcc error is certainly unfortunate though –  Oct 29 '21 at 16:12