2

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 of Delegater::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 -O3 or Release in VisualStudio) and without the printf in 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;
}
Solo
  • 105
  • 8
  • 2
    Please **[edit]** your question with an [mre] or [SSCCE (Short, Self Contained, Correct Example)](http://sscce.org) – NathanOliver Jul 06 '21 at 13:19
  • 4
    Different behavior with optimization enabled usually indicates *undefined behavior*. – Some programmer dude Jul 06 '21 at 13:19
  • @NathanOliver: I tried to keep it as minimal as possible, I don't know what to remove anymore from the working example (maybe the static_asserts at the beginning of main()) – Solo Jul 06 '21 at 13:22
  • @Solo: links to code are not accepted. – Fred Larson Jul 06 '21 at 13:25
  • 1
    If gcc and Visual Studio both crash with the same code, it is strong evidence that the problem is with the code and that it is not due to a compiler bug. – François Andrieux Jul 06 '21 at 13:25
  • 1
    To clarify FredLarson's comment, links to live examples are fine, but are not a substitute for directly providing code in the question which is required. Preferably provide a [MCVE]. – François Andrieux Jul 06 '21 at 13:26
  • @Some programmer dude: possible... but what specific undefined behavior could be? – Solo Jul 06 '21 at 13:27
  • @Solo The problem here is the code is too minimal. We need, here in the question, a minimal amount of code that is complete enough that we can copy it into our own compilers and try it. – NathanOliver Jul 06 '21 at 13:27
  • I'd suggest keeping the link but also just including the code at the bottom of the question. – jwimberley Jul 06 '21 at 13:29
  • Technically, all your `printf` calls... The format specifier `%p` requires a`void*` argument which you never pass. Mismatching formsat and argument type leads to UB (though that's probably not what's causing problems here). Why are you even using `printf` instead of the type-safe C++ streams? – Some programmer dude Jul 06 '21 at 13:30
  • 2
    About `static_assert(false)` which is ill formed: [if-constexpr-and-dependent-false-static-assert-is-ill-formed](https://stackoverflow.com/questions/57812038/if-constexpr-and-dependent-false-static-assert-is-ill-formed). – Jarod42 Jul 06 '21 at 13:32
  • 1
    Probably not the cause of your crash, but it looks like you are failing to forward `r` in `operator>>=(L& l, R&& r)`. I believe you should `std::move(r)` in `X{l, r};`. Though it looks like both overloads could be combined into one using [perfect forwarding](https://stackoverflow.com/questions/3582001/what-are-the-main-purposes-of-using-stdforward-and-which-problems-it-solves). – François Andrieux Jul 06 '21 at 13:33
  • @ François Andrieux, @ NathanOliver: thanks for clarification, I thought it would be too much to put the entire example in description, that's why I provided initially only a link. Added now the entire code – Solo Jul 06 '21 at 13:35
  • 1
    `= delete;` would be a good alternative to `static_assert(false);`. – Jarod42 Jul 06 '21 at 13:35
  • 2
    @Solo This is a complete example, but it is not minimal. To produce a MCVE you need to remove the irrelevant parts of the code. Try taking out various parts of your code to see if it is actually necessary to produce the problem. The less there is to look at, the more obvious the cause of the problem will become. You may even find the problem yourself in the process. – François Andrieux Jul 06 '21 at 13:39
  • 6
    `X` stores references to temporaries in its constructor, and then tries to use them in `operator()` after those temporaries are gone and references have become dangling. Whereupon the program exhibits undefined behavior. This is true in particular in case of `auto ddi = d0 >>= d1 >>= i;`, where an intermediate instance of `X` is produced. – Igor Tandetnik Jul 06 '21 at 14:07
  • @Igor Tandetnik: added a X::~X() dtor and it seems you're right: the dtor is called before the call to pipe(123). `(d0 >>= d1 >>= i)(123);` works ok but would not be a usable solution because temporaries will be created in each iteration if I call it in a loop. Any other idea? – Solo Jul 06 '21 at 14:26

1 Answers1

0

Here is the "Working solution" section I've added at the end of the OP.

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::move(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);
    }

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);
        //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;
}

Thanks everybody for your help!

Jerry Jeremiah
  • 9,045
  • 2
  • 23
  • 32
Solo
  • 105
  • 8