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
-O3
orRelease
in VisualStudio) and without theprintf
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;
}