Search code examples
c++17template-meta-programmingfunctorperfect-forwardingtemplate-argument-deduction

Perfectly forwarding function wrapper?


We consider the following functor, to help to detect what version of of its operator() is called.

struct functor
{
    void operator()() {std::cout << "functor::operator" << std::endl;}
    void operator()() const {std::cout << "functor::operator const" << std::endl;}
    void operator()() volatile {std::cout << "functor::operator volatile" << std::endl;}
    void operator()() const volatile {std::cout << "functor::operator const volatile" << std::endl;}
    void operator()(int) & {std::cout << "functor::operator &" << std::endl;}
    void operator()(int) const& {std::cout << "functor::operator const&" << std::endl;}
    void operator()(int) volatile& {std::cout << "functor::operator volatile&" << std::endl;}
    void operator()(int) const volatile& {std::cout << "functor::operator const volatile&" << std::endl;}
    void operator()(int) && {std::cout << "functor::operator &&" << std::endl;}
    void operator()(int) const&& {std::cout << "functor::operator const&&" << std::endl;}
    void operator()(int) volatile&& {std::cout << "functor::operator volatile&&" << std::endl;}
    void operator()(int) const volatile&& {std::cout << "const volatile&&" << std::endl;}
};

I would like to know, in C++17 (if doable of course) how to write two functor wrappers storing the function in a tuple and using class template argument deduction:

  • The first one wrapper1 that will call the operator depending on the qualifiers of the functor
  • The second one wrapper2 that will call the operator depending on the qualifiers of the wrapper

For example:

wrapper1 w1(functor{});
wrapper2 w2(functor{});
w1(0); // should call the && version since the functor was built as a temporary
w2(0); // should call the & version since the wrapper was built as a value

Here are some very rough exploration of the kind of things I am looking for. Note: this is just to give a "taste" of what I am thinking of.

template <class F>
struct wrapper_a
{
    constexpr wrapper_a(F f) noexcept: _tuple(f) {}
    template <class... Args>
    void operator()(Args&&... args)
    {std::get<0>(_tuple)(std::forward<Args>(args)...);}
    std::tuple<F> _tuple;
};

template <class F>
struct wrapper_b
{
    template <class G>
    constexpr wrapper_b(G&& g) noexcept: _tuple(std::forward<G>(g)) {}
    template <class... Args>
    void operator()(Args&&... args)
    {std::get<0>(_tuple)(std::forward<Args>(args)...);}
    std::tuple<F> _tuple;
};

template <class F> wrapper_b(F&& f) -> wrapper_b<F>;

template <class F>
struct wrapper_c
{
    template <class G>
    constexpr wrapper_c(G&& g) noexcept: _tuple(std::forward<G>(g)) {}
    template <class... Args>
    void operator()(Args&&... args)
    {std::forward<F>(std::get<0>(_tuple))(std::forward<Args>(args)...);}
    std::tuple<F> _tuple;
};

template <class F> wrapper_c(F&& f) -> wrapper_c<F>;

How to achieve what I am looking for? Is that even possible?


Solution

  • Not sure to understand your exactly requirements... and I'm a rookie with reference forwarding... anyway

    The first one wrapper1 that will call the operator depending on the qualifiers of the functor

    Maybe an expert can avoid this complication but seems to me that you need two template parameters

    template <typename F, typename G>
    struct wrapper1
    

    where F is the type of the copy of the parameter passed to the constructor and G is the intended type.

    So you have in it a F value

    F f;
    

    and you can use it trough forwarding

    template <typename ... Args>
    void operator() (Args && ... as) &
     { std::forward<G>(f)(std::forward<Args>(as)...); }
    

    To simplify, you can define a template deduction as follows

    template <typename F>
    wrapper1(F && f) -> wrapper1<std::decay_t<F>, decltype(std::forward<F>(f))>;
    

    The second one wrapper2 that will call the operator depending on the qualifiers of the wrapper

    Maybe I'm wrong but this seems to me a littler simpler.

    You need only a template parameter

    template <typename F>
    struct wrapper2
    

    and you only have to use std::move() in r-reference operators

    template <typename ... Args>
    void operator() (Args && ... as) &&
     { std::move(f)(std::forward<Args>(as)...);  }
    

    The deduction guide is simple

    template <typename F>
    wrapper2(F && f) -> wrapper2<std::decay_t<F>>;
    

    The following is a limited (to const/not-const and l-value/r-value reference alternatives) but full working examples.

    Observe that there are some compilation errors caused by constness problem (if you initialize a const wrapper1 with a not-const callable, there are problems)

    #include <iostream>
    
    struct functor
     {
       void operator()(int) & 
        {std::cout << "functor::operator &" << std::endl;}
       void operator()(int) const &
        {std::cout << "functor::operator const &" << std::endl;}
       void operator()(int) &&
        {std::cout << "functor::operator &&" << std::endl;}
       void operator()(int) const &&
        {std::cout << "functor::operator const &&" << std::endl;}
     };
    
    template <typename F, typename G>
    struct wrapper1
     {
       template <typename H>
       constexpr wrapper1 (H && f0) noexcept : f{std::forward<H>(f0)}
        {}
    
       template <typename ... Args>
       void operator() (Args && ... as) &
        { std::forward<G>(f)(std::forward<Args>(as)...); }
    
       template <typename ... Args>
       void operator() (Args && ... as) const &
        { std::forward<G>(f)(std::forward<Args>(as)...); }
    
       template <typename ... Args>
       void operator() (Args && ... as) &&
        { std::forward<G>(f)(std::forward<Args>(as)...);  }
    
       template <typename ... Args>
       void operator() (Args && ... as) const &&
        { std::forward<G>(f)(std::forward<Args>(as)...);  }
    
       F f;
     };
    
    template <typename F>
    wrapper1(F && f) 
       -> wrapper1<std::decay_t<F>, decltype(std::forward<F>(f))>;
    
    template <typename F>
    struct wrapper2
     {
       template <typename H>
       constexpr wrapper2 (H && f0) noexcept : f{std::forward<H>(f0)}
        {}
    
       template <typename ... Args>
       void operator() (Args && ... as) &
        { f(std::forward<Args>(as)...); }
    
       template <typename ... Args>
       void operator() (Args && ... as) const &
        { f(std::forward<Args>(as)...); }
    
       template <typename ... Args>
       void operator() (Args && ... as) &&
        { std::move(f)(std::forward<Args>(as)...);  }
    
       template <typename ... Args>
       void operator() (Args && ... as) const &&
        { std::move(f)(std::forward<Args>(as)...);  }
    
       F f;
     };
    
    template <typename F>
    wrapper2(F && f) -> wrapper2<std::decay_t<F>>;
    
    int main ()
     {
       functor       fc;
       functor const fd;
    
       wrapper1 w1a{functor{}};
       wrapper1 w1b{static_cast<functor const &&>(functor{})};
       wrapper1 w1c{fc};
       wrapper1 w1d{fd};
    
       wrapper1 const w1e{functor{}};
       wrapper1 const w1f{static_cast<functor const &&>(functor{})};
       wrapper1 const w1g{fc};
       wrapper1 const w1h{fd};
    
       w1a(0);
       w1b(0);
       w1c(0);
       w1d(0);
    
       std::cout << "----------------------------" << std::endl;
    
       // w1e(0); // compilation error
       w1f(0);
       // w1g(0); // compilation error
       w1h(0);
    
       std::cout << "----------------------------" << std::endl;
    
       wrapper1<functor, functor&&>{functor{}}(0);
       wrapper1<functor, functor const &&>
          {static_cast<functor const &&>(functor{})}(0);
       wrapper1<functor, functor&>{fc}(0);
       wrapper1<functor, functor const &>{fd}(0);
    
       std::cout << "----------------------------" << std::endl;
    
       // (wrapper1 <functor, functor&&> const)
          //{functor{}}(0); // compilation error
       (wrapper1<functor, functor const &&> const)
          {static_cast<functor const &&>(functor{})}(0);
       // (wrapper1<functor, functor&> const){fc}(0); // compilation error
       (wrapper1<functor, functor const &> const){fd}(0);
    
       wrapper2 w2a{functor{}};
       wrapper2 w2b{static_cast<functor const &&>(functor{})};
       wrapper2 w2c{fc};
       wrapper2 w2d{fd};
    
       wrapper2 const w2e{functor{}};
       wrapper2 const w2f{static_cast<functor const &&>(functor{})};
       wrapper2 const w2g{fc};
       wrapper2 const w2h{fd};
    
       std::cout << "----------------------------" << std::endl;
    
       w2a(0);
       w2b(0);
       w2c(0);
       w2d(0);
    
       std::cout << "----------------------------" << std::endl;
    
       w2e(0);
       w2f(0);
       w2g(0);
       w2h(0);
    
       std::cout << "----------------------------" << std::endl;
    
       wrapper2<functor>{functor{}}(0);
       wrapper2<functor>{static_cast<functor const &&>(functor{})}(0);
       wrapper2<functor>{fc}(0);
       wrapper2<functor>{fd}(0);
    
       std::cout << "----------------------------" << std::endl;
    
       (wrapper2<functor> const){functor{}}(0);
       (wrapper2<functor> const){static_cast<functor const &&>(functor{})}(0);
       (wrapper2<functor> const){fc}(0);
       (wrapper2<functor> const){fd}(0);
    
     }