Reputation: 1124
I have example code which compiles under C++14 fine on GCC/Clang/MSVC, and under C++17 on Clang/MSVC, yet under C++17 on GCC 8.x through 10.1 it produces an error.
#include <vector> // vector
template< typename Seq,
typename Seq::value_type& ( Seq::*next )(),
void ( Seq::*pop )() >
void f( Seq& );
template< typename Seq >
void g( Seq& seq )
{
f< Seq, &Seq::back, &Seq::pop_back >( seq );
}
void foo()
{
std::vector< int > v;
g( v );
}
I receive the following error from GCC 10.1 using CXXFLAGS=-std=c++17
:
<source>: In instantiation of 'void g(Seq&) [with Seq = std::vector<int>]':
<source>:17:10: required from here
<source>:11:41: error: no matching function for call to 'f<std::vector<int, std::allocator<int> >, (& std::vector<int, std::allocator<int> >::back), &std::vector<int, std::allocator<int> >::pop_back>(std::vector<int>&)'
11 | f< Seq, &Seq::back, &Seq::pop_back >( seq );
| ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^~~~~~~
<source>:6:6: note: candidate: 'template<class Seq, typename Seq::value_type& (Seq::* next)(), void (Seq::* pop)()> void f(Seq&)'
6 | void f( Seq& );
| ^
<source>:6:6: note: template argument deduction/substitution failed:
<source>:11:41: error: 'int& (std::vector<int>::*)(){((int& (std::vector<int>::*)())std::vector<int>::back), 0}' is not a valid template argument for type 'int& (std::vector<int>::*)()'
11 | f< Seq, &Seq::back, &Seq::pop_back >( seq );
| ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^~~~~~~
<source>:11:41: note: it must be a pointer-to-member of the form '&X::Y'
Compiler returned: 1
I know the second parameter, &Seq::back
is an overloaded function; I created a intermediate member function pointer of the non-const overload, and passed that as the second argument to call f
, yet I receive near identical errors.
So, the basic questions are, is this invalid C++17 code, or is this a GCC bug? Presuming it's invalid C++17, how would I make it valid?
Bonus question: what is 'int& (std::vector<int>::*)(){((int& (std::vector<int>::*)())std::vector<int>::back), 0}'
? I'm completely surprised by the {
/}
pair, and the 0
. I know the outer part is the method signature, and the inner first part is casting the overloaded method to the expected signature, but why the {
/}
pair and the 0
? An initializer list? A struct? The internals of a member pointer?
Upvotes: 2
Views: 574
Reputation: 1124
In C++17, 'noexcept' is now part of the type system, and 'libstdc++' adds 'noexcept' to several STL APIs, in this case 'back', 'front', and similar getters. This 'noexcept' addition is explicitly allowed by [res.on.exception]/5. Additionally, implicit casts from a 'noexcept' to a "non-noexcept" function pointer is explicitly allowed by [conv.fctptr]/1, so the question's code should still work. GCC is not implicitly converting the member function pointer correctly...
So, I can explicitly provide 'noexcept' overloads when compiling under GCC:
#include <vector> // vector
template< typename Seq,
typename Seq::value_type& ( Seq::*peek )(),
void ( Seq::*pop )() >
void f( Seq& );
#if defined( __GNUC__ ) && !defined( __clang__ )
template< typename Seq,
typename Seq::value_type& ( Seq::*peek )() noexcept,
void ( Seq::*pop )() >
void f( Seq& );
#endif // defined( __GNUC__ ) && !defined( __clang__ )
template< typename Seq >
void g( Seq& seq )
{
f< Seq, &Seq::back, &Seq::pop_back >( seq );
}
void foo()
{
std::vector< int > v;
g( v );
}
Upvotes: 0
Reputation: 29193
... [T]he behavior of a C++ program is unspecified (possibly ill-formed) if it attempts ... to form a pointer-to-member designating either a standard library non-static member function (
[member.functions]
) or an instantiation of a standard library member function template.
This is basically to allow implementors to add default template parameters and default arguments and all that whatnot to standard functions, which would make the types of their function pointers and references fail to line up with what the standard says is their interface. Also, the implementation can change those not-so-hidden parts of itself, and you shouldn't be depending on them. Hence a blanket ban (the exceptions—the addressable functions—seem to be in the iostream
and iomanip
stuff, generally). You're basically supposed to use lambdas or similar to wrap standard functions into their "expected" interfaces:
#include <vector>
// or just take functors
template<typename Seq, typename Seq::value_type &(*next)(Seq&), void (*pop)(Seq&)>
void f(Seq&);
template<typename Seq>
void g(Seq &seq) {
constexpr auto back = +[](Seq &s) -> typename Seq::value_type& { return s.back(); };
constexpr auto pop_back = +[](Seq &s) -> void { s.pop_back(); };
f<Seq, back, pop_back>(seq);
}
int main() {
std::vector<int> v;
g(v);
}
Note that, even though this explicit rule is new in C++20, allowances to the implementors to modify the "standard" presentation of functions have existed since before that. I.e. even though previous C++ versions didn't have this clause, your code still would not be portably correct, since a different implementation might not compile it.
Now, even with that allowance, I don't think GCC is out of the woods. For one, the above fails to compile with GCC due to a bug. Ok, fine, easy (though verbose) fix: lift back
and pop_back
out of g
(e.g. into a namespace detail
) to give them linkage. Other than that: note that on Compiler Explorer, both Clang and GCC use GCC's standard library by default. It's a super weird default (Clang only uses its own standard library under -stdlib=libc++
), but it tells us something: somehow GCC is failing to compile something that Clang thinks is perfectly fine. Technically, they're both abiding by the standard; there's no guarantee that your code will work. But we humans know that something's up. We can reduce the issue to
struct F { void foo() noexcept; }; // std::vector<int>::back is noexcept in libstdc++
constexpr inline void (F::*ptr)() = &F::foo;
template<void (F::*f)()> void g();
int main() { g<ptr>(); }
// Clang is happy, GCC is not
I'm going to mark the wonky error message down as "GCC's implementation details are leaking," as otherwise the expression in the error doesn't make much sense. Perhaps GCC keeps some extra flags or whatnot next to the function pointer (I know e.g. pointers to virtual
member functions cause the representations of pointers to member functions to be strange). Anyway, I believe this rejection is another GCC bug. The "pointer-to-noexcept
-member-function to pointer-to-member-function" conversion is perfectly valid in a converted constant expression (which a template non-type argument is), and putting such a converted &F::foo
into a constexpr
variable shows it. However, GCC simply doesn't like it for some reason. As you've discovered, a fix is to simply write two versions of your template: one for noexcept
and one for not-noexcept
.
TL;DR: Your code is broken, but GCC is probably even more broken.
Upvotes: 4