Search code examples
c++templatespipelinecompiler-optimization

C++ template behaves different when optimization is enabled (release)


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

Solution

  • 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!