Context:
- Invocable: class with operator() overloaded for some different sets of arguments
- Delegater: same as Invocable but using a delegate ("invocable") as 1st argument; different delegate.operator(ArgsA...)overloads can be called in each ofDelegater::operator(Delegate&& delegate, ArgsB...)(note ArgsA!=ArgsB)
- composition of a delegater with an invocable (resulting another "invocable") is done via right-associative operator>>=()
The target was to be able to write something like:
Delegater d0{0};//the ctor's arg is just an ID
Delegater d1{1};
Delegater d2{2};
Invocable i{9};
auto pipe = d0 >>= d1 >>= d2 >>= i;
pipe(1234);//=> d0(int=1234) calling d1(T1_int...) calling d2(T2...) calling i(...)
pipe(78.9);//=> d0(double=78.9) calling d1(T1_double...) calling d2(T2...) calling i(...)
Q0. My biggest question!
- with optimizations enabled (gcc -O3orReleasein VisualStudio) and without theprintfin X::X ctor (line~25) => runtime crash!
- without optimizations it works fine with or without printf.
Is the optimizer too aggressive and removes the X::X() ctor??? But it is too big of a coincidence to have the same compiler bug in both gcc & VisualStudio. What am I missing or doing wrong?
Q1. static_assert
- I have a static_assert in the generic overloaded Invocable::operator(Args&&...args)especially to catch unhandled cases like calling the Invocable::operator() with argument types for which there is no explicit overload (e.g "char*" in my example at line ~120).
- It works as expected in VisualStudio, generating a compile time assert when I try to call invocable("some text")... but gcc generates a compile time assert always, even if that line is commented out.
Is this a gcc bug or I do something wrong?
A working example: https://godbolt.org/z/MshrcvYKr
Complete minimal example:
#include <cassert>
#include <cstdio>
#include <source_location>
#include <type_traits>
#include <utility>
//----------------------------------------------------------------
struct Pipe{};//only for filter class tagging & overloaded operators constraining
namespace Pipeline{//right-associative pipe fitting
    struct X0{};//only for class tagging & overloaded operators constraining
    template<typename L,typename R>
    requires (std::derived_from<L,Pipe>)
    struct X//eXecutor: d>>=i, d>>=(d>>=i)
        : public X0
    {
        L& l;
        R& r;
        X(L& l, R& r)
            : l{l}
            , r{r}
        {
            printf("X{this=%p} ::X(l=%p, r=%p)\n", this, &l, &r);//commenting this line leads to crash with optimizations enabled! is ctor discarded???
        }
        template<typename...Args>
        requires (std::is_invocable_v<L,R,Args...>)
        auto operator()(Args&&...args) noexcept {
            return l(r, std::forward<Args>(args)...);
        }
    };
    template<typename L, typename R>
    requires (std::derived_from<L,Pipe> && !std::derived_from<R,Pipe> && !std::derived_from<R,X0>)
    auto operator>>=(L& l, R& r) noexcept {//for: lvalueDelegater0 >>= lvalueInvocable
        return X<L,R>{l, r};
    }
    template<typename L, typename R>
    requires (std::derived_from<L,Pipe> && std::derived_from<R,X0>)
    auto operator>>=(L& l, R&& r) noexcept {//for: lvaluePipe >>= rvalueX
        return X<L,R>{l, r};
    }
}
using Pipeline::operator>>=;
//----------------------------------------------------------------
struct Invocable{
    int id = 0;
    Invocable(int id=0) noexcept
        : id(id)
    {
        printf("Invocable{this=%p id=%d} ::Invocable(id=%d)\n", this, id, id);
    }
    template<typename...Args>
    void operator()(Args&&...args) noexcept {
        printf("ERR unhandled case! %s\n", std::source_location::current().function_name());
        //static_assert(false, "unhandled case");//works on VisualStudio but not on gcc
        assert(("unhandled case", false));//helps catching not handled cases
    }
    void operator()(int arg) noexcept {
        printf("Invocable{this=%p id=%d} ::%s(int=%d)\n", this, id, __func__, arg);
    }
    void operator()(double arg) noexcept {
        printf("Invocable{this=%p id=%d} ::%s(double=%lf)\n", this, id, __func__, arg);
    }
};
//----------------------------------------------------------------
struct Delegater
    : public Pipe
{
    int id = 0;
    Delegater(int id=0) noexcept
        : id(id)
    {
        printf("Delegater{this=%p id=%d} ::Delegater(id=%d)\n", this, id, id);
    }
public:
    template<typename Delegate, typename...Args>
    requires (std::is_invocable_v<Delegate,Args...>)
    void operator()(Delegate&& delegate, Args&&...args){//forwards to delegate
        printf("Delegater{this=%p id=%d} ::%s(delegate=%p, args...) %s\n", this, id, __func__, &delegate, std::source_location::current().function_name());
        delegate(std::forward<decltype(args)>(args)...);
    }
    template<typename Delegate>
    requires (std::is_invocable_v<Delegate, double>)
    void operator()(Delegate&& delegate, int arg){
        printf("Delegater{this=%p id=%d} ::%s(delegate=%p, int=%d)\n", this, id,  __func__, &delegate, arg);
        delegate((double)arg);//invoke delegate with some args (not necessary the same as Args...)
    }
    template<typename Delegate>
    //requires (std::is_invocable_v<Delegate, int>)
    void operator()(Delegate&& delegate, double arg){
        printf("Delegater{this=%p id=%d} ::%s(delegate=%p, double=%lf)\n", this, id,  __func__, &delegate, arg);
        delegate((int)arg);//invoke delegate with some args (not necessary the same as Args...)
    }
};
//----------------------------------------------------------------
int main(){
    printf("-------- creation\n");
    Delegater d0{0};
    Delegater d1{1};
    Delegater d2{2};
    Invocable i{9};
    //i("123");//why I cannot catch this at compile time with gcc???
    printf("-------- d0 >>= i\n");
    auto di = d0 >>= i;
    di(123);
    printf("-------- d0 >>= d1 >>= i\n");
    auto ddi = d0 >>= d1 >>= i;
    ddi(123);
    return 0;
}
Answers:
A0. Thanks @Igor Tandetnik for noticing that "my biggest question"  was actually my BIG and elementary mistake: forgot about temporary instances that are destroyed as soon as they are not needed in expression, before actually using my "pipeline"! It was a simple coincidence that it worked with the afore mentioned printf
A1. Thanks @Jarod42 for explanation and the better solution: use =delete; to detect unhandled cases at compile time
template<typename...Args> void operator()(Args&&...args) noexcept = delete;
Also the comment from @François Andrieux, the one regarding perfect forwarding, also helped me to find the final solution:
- in "composition" catch instances of Delegater & Invocable by reference (in struct Di{...})
- catch temporary objects using perfect forwarding (in struct Ddi{...})
Working solution: https://godbolt.org/z/PdzTT1YGs
#include <cassert>
#include <cstdio>
#include <source_location>
#include <type_traits>
#include <utility>
//----------------------------------------------------------------
struct Pipe{};      //only for filter class tagging & overloaded operators constraining
namespace Pipeline{//right-associative pipe fitting
    struct Di0{};   //only for class tagging & overloaded operators constraining
    struct Ddi0{};  //only for class tagging & overloaded operators constraining
    template<typename L,typename R>
    requires (std::derived_from<L,Pipe> && !std::derived_from<R,Pipe> && !std::derived_from<R,Di0> && !std::derived_from<R,Ddi0>)
    struct Di   //d >>= i
        : public Di0
    {
        L& l;
        R& r;
        Di(L& l, R& r)
            : l(l)
            , r(r)
        {
            //printf("%s{this=%p} ::%s(&l=%p, &r=%p)\n", __func__, this, __func__, &l, &r);
        }
        Di(L& l, R&& r) = delete;
        ~Di(){
            //printf("%s{this=%p &l=%p, &r=%p} ::%s()\n", __func__+1, this, &l, &r, __func__);
        }
        template<typename...Args>
        requires (std::is_invocable_v<L,R,Args...>)
        auto operator()(Args&&...args) noexcept {
            //printf("Di{this=%p &l=%p, &r=%p} ::%s()\n", this, &l, &r, __func__);
            return l(r, std::forward<Args>(args)...);
        }
    };
    template<typename L,typename R>
    requires (std::derived_from<L,Pipe> && (std::derived_from<R,Di0> || std::derived_from<R,Ddi0>))
    struct Ddi  //d >>= d >>= i
        : public Ddi0
    {
        L& l;
        R  r;
        Ddi(L& l, R& r) = delete;
        Ddi(L& l, R&& r)
            : l{l}
            , r{std::forward<R>(r)}
        {
            //printf("%s{this=%p &l=%p r=%p} ::%s(&l=%p, &&r=%p) this->r=std::move(r)\n", __func__, this, &this->l, &this->r, __func__, &l, &r);
        }
        ~Ddi(){
            //printf("%s{this=%p &l=%p r=%p} ::%s()\n", __func__+1, this, &l, &r, __func__);
        }
        template<typename...Args>
        requires (std::is_invocable_v<L,R,Args...>)
        auto operator()(Args&&...args) noexcept {
            //printf("Ddi{this=%p &l=%p r=%p} ::%s()\n", this, &l, &r, __func__);
            return l(r, std::forward<Args>(args)...);
        }
    };
    template<typename L, typename R>
    requires (std::derived_from<L,Pipe> && !std::derived_from<R,Pipe> && !std::derived_from<R,Di0> && !std::derived_from<R,Ddi0>)
    auto operator>>=(L& l, R& r) noexcept {//for: lvalueDelegater0 >>= lvalueInvocable
        return Di<L,R>{l, r};
    }
    template<typename L, typename R>
    requires (std::derived_from<L,Pipe> && (std::derived_from<R,Di0> || std::derived_from<R,Ddi0>))
    auto operator>>=(L& l, R&& r) noexcept {//for: lvaluePipe >>= lvaluePipe >>= ... >>= lvalueInvocable
        return Ddi<L,R>{l, std::move(r)};
    }
}
using Pipeline::operator>>=;
//----------------------------------------------------------------
struct Invocable{
    int id = 0;
    Invocable(int id=0) noexcept
        : id(id)
    {
        printf("Invocable{this=%p id=%d} ::Invocable(id=%d)\n", this, id, id);
    }
    template<typename...Args>
    void operator()(Args&&...args) noexcept = delete;//helps catching not handled cases at compile time
    void operator()(int arg) noexcept {
        printf("Invocable{this=%p id=%d} ::%s(int=%d)\n", this, id, __func__, arg);
    }
    void operator()(double arg) noexcept {
        printf("Invocable{this=%p id=%d} ::%s(double=%lf)\n", this, id, __func__, arg);
    }
};
//----------------------------------------------------------------
struct Delegater
    : public Pipe
{
    int id = 0;
    Delegater(int id=0) noexcept
        : id(id)
    {
        printf("Delegater{this=%p id=%d} ::Delegater(id=%d)\n", this, id, id);
    }
    template<typename Delegate, typename...Args>
    requires (std::is_invocable_v<Delegate,Args...>)
    void operator()(Delegate&& delegate, Args&&...args){//forwards to delegate
        printf("Delegater{this=%p id=%d} ::%s(delegate=%p, args...) %s\n", this, id, __func__, &delegate, std::source_location::current().function_name());
        delegate(std::forward<decltype(args)>(args)...);
    }
    template<typename Delegate>
    requires (std::is_invocable_v<Delegate, double>)
    void operator()(Delegate&& delegate, int arg){
        printf("Delegater{this=%p id=%d} ::%s(delegate=%p, int=%d)\n", this, id,  __func__, &delegate, arg);
        //invoke delegate with some args (not necessary the same as Args...)
        //delegate((int)arg);
        delegate((double)arg);
    }
    template<typename Delegate>
    requires (std::is_invocable_v<Delegate, int>)
    void operator()(Delegate&& delegate, double arg){
        printf("Delegater{this=%p id=%d} ::%s(delegate=%p, double=%lf)\n", this, id,  __func__, &delegate, arg);
        //invoke delegate with some args (not necessary the same as Args...)
        delegate((int)arg);
    }
};
//----------------------------------------------------------------
int main(){
    printf("-------- creation\n");
    Delegater d0{0};
    Delegater d1{1};
    Delegater d2{2};
    Delegater d3{3};
    Invocable i{9};
    static_assert(!std::is_invocable_v<Invocable,char*>);
    //i("123");//catched @compile time
    printf("-------- d0 >>= i\n");
    auto di = d0 >>= i;
    di(123);
    printf("-------- d0 >>= d1 >>= i\n");
    auto ddi = d0 >>= d1 >>= i;
    ddi(123);
    printf("-------- d0 >>= d1 >>= d2 >>= i\n");
    auto dddi = d0 >>= d1 >>= d2 >>= i;
    dddi(123);
    printf("-------- d0 >>= d1 >>= d2 >>= d3 >>= i\n");
    auto ddddi = d0 >>= d1 >>= d2 >>= d3 >>= i;
    ddddi(123);
    printf("END\n");
    return 0;
}
 
    