[BUG] CPP2_UFCS macros don't work with template functions/methods with multiple template arguments
CPP2_UFCS macro is based on variadic macros. That means that every comma , in the macro separates two arguments for the macro (unless surrounded by parentheses ().
That makes below the cpp2 code (mixed mode):
#include <string>
template <auto from, auto to>
auto substr(const std::string& input) -> std::string {
return input.substr(from, to-from);
}
main: () -> int = {
test_string: std::string = "Unfortunatelly macros have limitations";
cmd.substr<3,6>();
}
compiles to (skipping boilerplate):
[[nodiscard]] auto main() -> int{
std::string test_string { "Unfortunatelly macros have limitations" };
CPP2_UFCS_0(substr<3,6>, test_string);
}
And a call to CPP2_UFCS_0 will be perceived by the compiler as CPP2_UFCS_0((substr<3),(6>), (test_string)); (parentheses added to emphasize the problem. That will not compile and will end up with an error:
clang++ -std=c++20 -Iinclude ../tests/ufcs-method-limitations.cpp
tests/ufcs-method-limitations.cpp2:10:30: error: too many arguments provided to function-like macro invocation
CPP2_UFCS_0(substr<3,6>, test_string);
^
include/cpp2util.h:487:9: note: macro 'CPP2_UFCS_0' defined here
#define CPP2_UFCS_0(FUNCNAME,PARAM1) \
^
tests/ufcs-method-limitations.cpp2:10:5: error: use of undeclared identifier 'CPP2_UFCS_0'
CPP2_UFCS_0(substr<3,6>, test_string);
^
2 errors generated.
A similar error will happen for CPP2_UFCS version - that reminds me why we hate macros so much.
Normally, in such cases, we would add parentheses to give hints to the preprocessor where are the arguments. In this case when we do it we will look like:
CPP2_UFCS_0((substr<3,6>), test_string);
But it will also fail to compile with the error: expected unqualified-id:
clang++ -std=c++20 -Iinclude ../tests/ufcs-method-limitations.cpp
tests/ufcs-method-limitations.cpp2:10:17: error: expected unqualified-id
CPP2_UFCS_0((substr<3,6>), test_string);
^
tests/ufcs-method-limitations.cpp2:10:17: error: expected unqualified-id
error: constexpr if condition is not a constant expression
tests/ufcs-method-limitations.cpp2:10:5: note: in instantiation of function template specialization 'main()::(anonymous class)::operator()<std::string &>' requested here
CPP2_UFCS_0((substr<3,6>), test_string);
^
include/cpp2util.h:494:2: note: expanded from macro 'CPP2_UFCS_0'
}(PARAM1)
^
3 errors generated.
Unless there is a trick to make that macro work again we will need to come up with some other idea to solve it.
I found that bug while trying to update the function call chaining PR (https://github.com/hsutter/cppfront/pull/18). Probably working UFCS will not be possible without semantic analysis on cppfront side.
The same will be needed to allow Unified "." member/scope selection (e.g., std.swap(x,y)) - it is impossible to distinguish if std is a namespace by using require expressions or decltype hacks (already tried that).
I have tried some tricks with expanding macros and packing FUNCNAME into an additional macro:
#define CPP2_EXPAND(...) CPP2_E(__VA_ARGS__)
#define CPP2_E(...) __VA_ARGS__
#define CPP2_UFCS_FUNCNAME(...) CPP2_EXPAND __VA_ARGS__
#define CPP2_UFCS_0(FUNCNAME,PARAM1) \
[](auto&& obj) { \
if constexpr (requires{ std::forward<decltype(obj)>(obj).CPP2_UFCS_FUNCNAME(FUNCNAME)(); }) { \
return std::forward<decltype(obj)>(obj).CPP2_UFCS_FUNCNAME(FUNCNAME)(); \
} else { \
return CPP2_UFCS_FUNCNAME(FUNCNAME)(std::forward<decltype(obj)>(obj)); \
} \
}(PARAM1)
and I have manually edit generated code to:
CPP2_UFCS_0((substr<3,6>), test_string);
That ends up with another error:
clang++ -std=c++20 -Iinclude ../tests/ufcs-method-limitations.cpp
tests/ufcs-method-limitations.cpp2:10:18: error: missing 'template' keyword prior to dependent template name 'substr'
CPP2_UFCS_0((substr<3,6>), test_string);
^ ~~~~~
include/cpp2util.h:494:81: note: expanded from macro 'CPP2_UFCS_0'
if constexpr (requires{ std::forward<decltype(obj)>(obj).CPP2_UFCS_FUNCNAME(FUNCNAME)(); }) { \
^~~~~~~~
include/cpp2util.h:490:45: note: expanded from macro 'CPP2_UFCS_FUNCNAME'
#define CPP2_UFCS_FUNCNAME(...) CPP2_EXPAND __VA_ARGS__
^~~~~~~~~~~
include/cpp2util.h:487:33: note: expanded from macro 'CPP2_EXPAND'
#define CPP2_EXPAND(...) CPP2_E(__VA_ARGS__)
^~~~~~~~~~~
include/cpp2util.h:488:21: note: expanded from macro 'CPP2_E'
#define CPP2_E(...) __VA_ARGS__
^~~~~~~~~~~
tests/ufcs-method-limitations.cpp2:10:18: error: missing 'template' keyword prior to dependent template name 'substr'
CPP2_UFCS_0((substr<3,6>), test_string);
^ ~~~~~
include/cpp2util.h:495:68: note: expanded from macro 'CPP2_UFCS_0'
return std::forward<decltype(obj)>(obj).CPP2_UFCS_FUNCNAME(FUNCNAME)(); \
^~~~~~~~
include/cpp2util.h:490:45: note: expanded from macro 'CPP2_UFCS_FUNCNAME'
#define CPP2_UFCS_FUNCNAME(...) CPP2_EXPAND __VA_ARGS__
^~~~~~~~~~~
include/cpp2util.h:487:33: note: expanded from macro 'CPP2_EXPAND'
#define CPP2_EXPAND(...) CPP2_E(__VA_ARGS__)
^~~~~~~~~~~
include/cpp2util.h:488:21: note: expanded from macro 'CPP2_E'
#define CPP2_E(...) __VA_ARGS__
^~~~~~~~~~~
error: constexpr if condition is not a constant expression
tests/ufcs-method-limitations.cpp2:10:5: note: in instantiation of function template specialization 'main()::(anonymous class)::operator()<std::string &>' requested here
CPP2_UFCS_0((substr<3,6>), test_string);
^
include/cpp2util.h:499:2: note: expanded from macro 'CPP2_UFCS_0'
}(PARAM1)
^
3 errors generated.
Using a pointer to member function seems to compile fine on all major compilers. It doesn't seem ideal though, as it still warns about the template keyword missing on gcc.
#define CPP2_EXPAND(...) CPP2_E(__VA_ARGS__)
#define CPP2_E(...) __VA_ARGS__
#define CPP2_UFCS_FUNCNAME(...) CPP2_EXPAND __VA_ARGS__
#define CPP2_UFCS_0(FUNCNAME,PARAM1) \
[](auto&& obj) { \
if constexpr (requires{ decltype(obj)::CPP2_UFCS_FUNCNAME(FUNCNAME)(std::forward<decltype(obj)>(obj)); }) { \
return decltype(obj)::CPP2_UFCS_FUNCNAME(FUNCNAME)(std::forward<decltype(obj)>(obj)); \
} else { \
return CPP2_UFCS_FUNCNAME(FUNCNAME)(std::forward<decltype(obj)>(obj)); \
} \
}(PARAM1)
I haven't tested this thoroughly nor against what the standards says but maybe a starting point? One problem I see with this is it'll also match static class members, which is not really what we want.
Edit: I tried using std::mem_fn and std::is_member_function_pointer to filter class members in the requires expression, but to no avail. For some reason doing anything else than calling the member function in place leads to a syntax error diagnostic (on clang trunc at least).
Thanks everyone... In the meantime I think it's probably enough of a corner case that we don't need to block other progress on this, so until we have a solution that works, I think the simplest is to just not emit UFCS in the multi-template-arg case that doesn't work with the macro. I'll commit that workaround now as an alpha limitation, but please do reopen this (or create a new issue or PR) if you have a better solution that makes it work!
I have found a solution for handling methods with variadic number of arguments - included here: https://github.com/hsutter/cppfront/pull/169#issuecomment-1359292705