0

I asked previously about the following class, which is a wrapper around a member function. I now want to add "plugin" functionality to it via a proxy class. My wrapper class's operator* returns a proxy object on which I can then assign and retrieve as shown below:

#include <cstdio>
#include <type_traits>
#include <utility>

using namespace std;

class testclass {
public:
  double get() { return d_; }
  void set(double d) { d_ = d; }
  double d_ = 0.0;
};

template <typename PropertyType>
struct DEFAULT_WRAPPER_PROXY {
  DEFAULT_WRAPPER_PROXY(PropertyType* p) : property_(p) {}

  operator typename PropertyType::GetterReturnType() {
    return property_->Get();
  }

  DEFAULT_WRAPPER_PROXY& operator=(typename PropertyType::GetterReturnType val) {
    property_->Set(val);
    return *this;
  }

  PropertyType* property_;
};

template <typename PropertyType>
struct LOGGING_WRAPPER_PROXY {
  LOGGING_WRAPPER_PROXY(PropertyType* p) : property_(p) {}

  operator typename PropertyType::GetterReturnType() {
    // Log some interesting stuff
    return property_->Get();
  }

  LOGGING_WRAPPER_PROXY& operator=(typename PropertyType::GetterReturnType val) {
    // Log some interesting stuff
    property_->Set(val);
    return *this;
  }

  PropertyType* property_;
};

template<typename Retriever, typename Updater, typename OwningClass, template<typename PropertyType> class WRAPPER_PROXY = DEFAULT_WRAPPER_PROXY>
struct Wrapper {
  Wrapper(Retriever retriever, Updater updater, OwningClass* owner) : retriever_(retriever), updater_(updater), containingClass_(owner) {}

  using GetterReturnType = std::invoke_result_t<Retriever, OwningClass>;
  GetterReturnType Get() { return (containingClass_->*retriever_)(); }

  template<typename...Args>
  using SetterReturnType = std::invoke_result_t<Updater, OwningClass, Args...>;

  template<typename...Args>
  SetterReturnType<Args...> Set(Args&&... args) { return (containingClass_->*updater_)((forward<Args>(args))...); }

  WRAPPER_PROXY<Wrapper<Retriever, Updater, OwningClass>> operator*() {
    return WRAPPER_PROXY(this);
  }

  Retriever retriever_;
  Updater updater_;
  OwningClass* containingClass_;
};


int main() {

  testclass tc;

  {
    // Can use template arg deduction in construction
    Wrapper pp(&testclass::get, &testclass::set, &tc);
    // use default proxy
    double y = *pp;
    *pp = 102;
  }

  {
    // Try and use the logging proxy
    // Does not work: LWT is not a template
    //using LWT = LOGGING_WRAPPER_PROXY<Wrapper<decltype(testclass::get), decltype(testclass::set), testclass>>;
    //Wrapper<decltype(testclass::get), decltype(testclass::set), testclass,  LWT> pp2(&testclass::get, &testclass::set, &tc);

    // Does not work;see errors below
    Wrapper<decltype(testclass::get), decltype(testclass::set), testclass, LOGGING_WRAPPER_PROXY> pp2(&testclass::get, &testclass::set, &tc);

  }

}

The errors I get are as follows:

'std::invoke_result_t' : Failed to specialize alias template
'type': is not a member of any direct or indirect base class of 'std::_Invoke_traits_nonzero<void,Retriever,OwningClass>'

Can anyone suggest a nice way I can achieve this plugin functionality please?

Wad
  • 1,454
  • 1
  • 16
  • 33

2 Answers2

2

You were missing a & from the decltype expressions when trying to instantiate the Wrapper template:

Wrapper<
    decltype(&testclass::get),
             ^
    decltype(&testclass::set),
             ^
    testclass, 
    LOGGING_WRAPPER_PROXY
> pp2(&testclass::get, &testclass::set, &tc);

Generally, when working with the MSVC compiler (Visual Studio C++), I would recommend utilizing godbolt with hard-to-understand compiler error messages. Microsoft's compiler has a tendency to give the worst error messages among the three well-known compilers (clang, gcc, and msvc), especially with templates.

For example, your code in godbolt produces rather clear error messages with the three compilers. Unfortunately, I was not able to reproduce the exact error message you were having, although it seemed that the missing & were the only problem.

P.S. Regarding your duplication problem again (having to decltype each of the constructor params), I would define a factory function to help with this:

template<
    template<typename PropertyType> 
        class WRAPPER_PROXY = DEFAULT_WRAPPER_PROXY, 
    typename Retriever, 
    typename Updater, 
    typename OwningClass>
auto MakeWrapper(Retriever&& retriever, Updater&& updater, OwningClass* const owner)
{
    return Wrapper<Retriever, Updater, OwningClass, WRAPPER_PROXY>(
        std::forward<Retriever>(retriever), 
        std::forward<Updater>(updater), 
        owner
    );
}

Function templates allow us to partially deduce the template arguments, which is not possible with class templates. This makes it easy to only modify the WRAPPER_PROXY template argument, as everything else would be deduced from the Wrapper's constructor call anyways (or in this case the MakeWrapper's function arguments):

// Error-prone, lengthy and repetitive
Wrapper<
    decltype(&testclass::get), 
    decltype(&testclass::set), 
    testclass, 
    LOGGING_WRAPPER_PROXY
> pp2(&testclass::get, &testclass::set, &tc);

// Cleaner (using CTAD here, could also use auto)
Wrapper pp3 = MakeWrapper<LOGGING_WRAPPER_PROXY>(
    &testclass::get, 
    &testclass::set, 
    &tc
);

// The types are the same
static_assert(std::is_same_v<decltype(pp2), decltype(pp3)>);
Rane
  • 321
  • 2
  • 6
  • 1
    Thank you; excellent idea using the factory too. Can you clarify though - how come the default argument for `MakeWrapper` does not have to be specified last, like a normal function argument would? I've never seen this before... – Wad Jul 10 '22 at 19:22
  • @Wad At first glance, I will admit that it seems strange. I was half-expecting it to not compile as well. In this case, though, the compiler is always able to deduce the template parameters following the default parameter from the function parameters (see [this question](https://stackoverflow.com/q/33690863/14462794)). – Rane Jul 10 '22 at 20:09
2

The issue here is the result of the fact that you're not using decltype on a function pointer. It should be

Wrapper<decltype(&testclass::get), decltype(&testclass::set), testclass, LOGGING_WRAPPER_PROXY> pp2(&testclass::get, &testclass::set, &tc);

If you don't insist on having type aliases as part of the Wrapper class, you could rewrite the code a bit using a deduced return type:

template<class T>
using GetterReturnType = decltype(std::declval<T>().Get());

template <typename PropertyType>
struct DEFAULT_WRAPPER_PROXY {
    DEFAULT_WRAPPER_PROXY(PropertyType* p) : property_(p) {}

    operator GetterReturnType<PropertyType>() {
        return property_->Get();
    }

    DEFAULT_WRAPPER_PROXY& operator=(GetterReturnType<PropertyType> val) {
        property_->Set(val);
        return *this;
    }

    PropertyType* property_;
};

template <typename PropertyType>
struct LOGGING_WRAPPER_PROXY {
    LOGGING_WRAPPER_PROXY(PropertyType* p) : property_(p) {}

    operator GetterReturnType<PropertyType>() {
        // Log some interesting stuff
        return property_->Get();
    }

    LOGGING_WRAPPER_PROXY& operator=(typename PropertyType::GetterReturnType val) {
        // Log some interesting stuff
        property_->Set(val);
        return *this;
    }

    PropertyType* property_;
};

template<typename Retriever, typename Updater, typename OwningClass, template<typename PropertyType> class WRAPPER_PROXY = DEFAULT_WRAPPER_PROXY>
struct Wrapper {
    Wrapper(Retriever retriever, Updater updater, OwningClass* owner) : retriever_(retriever), updater_(updater), containingClass_(owner) {}

    decltype(auto) Get() { return std::invoke(retriever_, containingClass_); }

    template<typename...Args>
    decltype(auto) Set(Args&&... args) { return std::invoke(updater_, containingClass_, forward<Args>(args)...); }

    WRAPPER_PROXY<Wrapper<Retriever, Updater, OwningClass>> operator*() {
        return WRAPPER_PROXY(this);
    }

    Retriever retriever_;
    Updater updater_;
    OwningClass* containingClass_;
};
fabian
  • 80,457
  • 12
  • 86
  • 114
  • Thanks, this is good. I'd previously stayed away from `invoke` as I thought it might be slower than a direct call to a member function, but looking at the asm generated it seems to be the same. – Wad Jul 10 '22 at 19:21
  • Also, how come `template using GetterReturnType = decltype(std::declval().Get());` compiles? The [documentation](https://en.cppreference.com/w/cpp/utility/declval) says there has to be a default constructor for T, which has been used as the Wrapper class, which has no default constructor? – Wad Jul 10 '22 at 19:41
  • @Wad Try to steer away from making such assumptions. Numerous utilities reside in the standard library, which once compiled will generate code exactly as if done manually, but while being much more readable (eg. array, algorithm, functional, numeric, utility). Looking at the possible implementation for `std::invoke` [here](https://en.cppreference.com/w/cpp/utility/functional/invoke) after all the `if constexpr`'s it would boil down to `(std::forward(t1).*f)(std::forward(args)...);`, which is what you were, more or less, doing manually before. – Rane Jul 10 '22 at 20:39
  • @Wad You are probably referring to this code in the example `// decltype(NonDefault().foo()) n2 = n1; // error: no default constructor`. Do note that this is the very problem `std::declval` is designed to solve: Converts **any type T** to a reference type, making it possible to use member functions in decltype expressions **without the need to go through constructors**. Meaning `std::declval` never calls any constructors, and thus it does not require them to exist. – Rane Jul 10 '22 at 20:45
  • @Wad `std::declval` is only supposed to be used in an unevaluated context and in an unevaluated context only the function signature is relevant. In fact you're quite likely to have a implementation of the function body that results in a compiler error, should the compiler be required to compile the body, see [the "Possible Implementation" section here](https://en.cppreference.com/w/cpp/utility/declval); inside the fact that it's used inside `decltype()` means it's used in an unevaluated context. – fabian Jul 10 '22 at 20:54
  • Thank you fabian. I have ultimately accepted Rane's answer; both were correct (as I know now!) but @Rane gave the idea for the factory, hence that gave that answer the edge. – Wad Jul 12 '22 at 00:35