GCC C++14/17 成员函数指针模板参数的区别

Posted

技术标签:

【中文标题】GCC C++14/17 成员函数指针模板参数的区别【英文标题】:GCC C++14/17 Difference for Member Function Pointer Template Parameters 【发布时间】:2020-07-20 22:32:05 【问题描述】:

我的示例代码在 GCC/Clang/MSVC 上可以在 C++14 下编译,在 Clang/MSVC 上可以在 C++17 下编译,但在 GCC 8.x 到 10.1 上的 C++17 下会产生错误.

#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 );

我使用 CXXFLAGS=-std=c++17 从 GCC 10.1 收到以下错误:

<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

Compiler Explorer

我知道第二个参数,&amp;Seq::back是一个重载函数;我创建了一个非常量重载的中间成员函数指针,并将其作为第二个参数传递给调用f,但我收到几乎相同的错误。

所以,基本问题是,这是无效的 C++17 代码,还是 GCC 错误?假设它是无效的 C++17,我将如何使它有效?

额外问题:'int&amp; (std::vector&lt;int&gt;::*)()((int&amp; (std::vector&lt;int&gt;::*)())std::vector&lt;int&gt;::back), 0' 是什么?我对/0 感到非常惊讶。我知道外部是方法签名,内部的第一部分是将重载的方法转换为预期的签名,但是为什么/ 对和0?初始化列表?一个结构?成员指针的内部结构?

【问题讨论】:

【参考方案1】:

[namespace.std]/6

... [T] 如果 C++ 程序试图... 形成一个指向成员的指针来指定标准库非静态成员函数 (@987654326 @) 或标准库成员函数模板的实例化。

这基本上是为了允许实现者向标准函数添加默认模板参数和默认参数以及所有其他内容,这将使其函数指针和引用的类型无法与标准所说的接口一致。此外,实现可以更改自身那些不那么隐藏的部分,您不应该依赖它们。因此,一揽子禁令(例外情况——可寻址函数——似乎一般在iostreamiomanip 中)。您基本上应该使用 lambdas 或类似方法将标准函数包装到它们的“预期”接口中:

#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);

请注意,尽管这个明确的规则是 C++20 中的新规则,但在此之前就已经允许实现者修改函数的“标准”表示。 IE。即使以前的 C++ 版本没有此子句,您的代码 仍然 也不会是正确的,因为不同的实现可能无法编译它。

现在,即使有这笔津贴,我认为 GCC 也没有脱离险境。一方面,上述内容无法使用 GCC due to a bug 编译。好的,很好,简单(虽然很冗长)修复:将 backpop_backg 中提升出来(例如,放到 namespace detail 中)以给它们链接。除此之外:请注意,在 Compiler Explorer 上,默认情况下,Clang 和 GCC 都使用 GCC 的标准库。这是一个超级怪异的默认设置(Clang 只在-stdlib=libc++ 下使用它自己的标准库),但它告诉我们一些事情:不知何故,GCC 无法编译 Clang 认为非常好的东西。从技术上讲,它们都遵守标准。无法保证您的代码可以正常工作。但我们人类知道有什么事情发生了。我们可以将问题简化为

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

我会将不稳定的错误消息标记为“GCC 的实现细节正在泄漏”,否则错误中的表达式没有多大意义。也许 GCC 在函数指针旁边保留了一些额外的标志或诸如此类的东西(我知道,例如指向 virtual 成员函数的指针会导致指向成员函数的指针的表示很奇怪)。无论如何,我相信这个拒绝是另一个 GCC 错误。 "pointer-to-noexcept-member-function to pointer-to-member-function" conversion 在转换后的常量表达式(模板非类型参数是)中完全有效,并将这样一个转换后的 @ 987654338@ 变成constexpr 变量显示它。但是,出于某种原因,GCC 根本不喜欢它。正如您所发现的,解决方法是简单地编写模板的两个版本:一个用于noexcept,一个用于非noexcept

TL;DR:您的代码已损坏,但 GCC 可能更多已损坏。

【讨论】:

您的调查使我朝着正确的方向前进。但是,Addressable Functions 似乎是 C++20 的限制。我相信 C++17 中关于“noexcept”的变化更直接地解释了我的问题。 en.cppreference.com/w/cpp/language/… @CharlesLWilcox:请记住,C++20 规则并不是全新的:C++17 中的 [member.functions]/2 已经允许使用诸如默认参数之类的技巧,这些技巧会触发指向成员函数。【参考方案2】:

在 C++17 中,“noexcept”现在是类型系统的一部分,“libstdc++”将“noexcept”添加到多个 STL API,在本例中为“back”、“front”和类似的 getter。 [res.on.exception]/5 明确允许此“noexcept”添加。此外,[conv.fctptr]/1 明确允许从“noexcept”到“non-noexcept”函数指针的隐式转换,因此问题的代码应该仍然有效。 GCC 没有正确地隐式转换成员函数指针...

因此,在 GCC 下编译时,我可以显式提供“noexcept”重载:

#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 );

Compiler Explorer

【讨论】:

允许实现添加noexcept(但不能添加constexpr)。此外,从 C++20 开始,您根本不允许形成指向标准库(成员)函数的指针,如另一个答案中所述。 @DavisHerring 谢谢,我找到了允许这样做的标准描述,并修改了我的答案。 指向 noexcept 函数的指针向上转换为指向 not-noexcept 函数的指针。你不应该需要像这样重载。 GCC 只是很奇怪。 @HTNW 啊,这是一个公平的观点。我会更新答案。 en.cppreference.com/w/cpp/language/…

以上是关于GCC C++14/17 成员函数指针模板参数的区别的主要内容,如果未能解决你的问题,请参考以下文章

C++11/14/17,GCC 7 与 GCC 8:朋友类模板的名称查找

使用带有模板参数的成员函数指针

泛型成员函数指针作为模板参数

在函数c ++中通过引用传递的指针参数

类数据成员中可以使用模板参数推导吗?

c++模板使用与成员函数指针