4

I would like to specialize a template to do one thing on pointers to data members and another thing on pointers to member functions. This used to work up until gcc 11, with member functions acting as more specific. It still works with clang 11, but seems to have broken with gcc.

Here is a minimal non-working example:

#include <iostream>

template<auto F> struct which;

template<typename K, typename V, K V::*F>
struct which<F> {
  static constexpr char desc[] = "pointer to data member";
};

template<typename K, typename V, K (V::*F)()>
struct which<F> {
  static constexpr char desc[] = "pointer to member function";
};

struct S {
  int i;
  int f() { return 0; }
};

int
main()
{
  std::cout << "S::i: " << which<&S::i>::desc << std::endl;
  std::cout << "S::f: " << which<&S::f>::desc << std::endl;
}

As of gcc 11.1, compiling with g++ -std=c++17 memptr.cc gives:

memptr.cc: In function 'int main()':
memptr.cc:24:40: error: ambiguous template instantiation for 'struct which<&S::f>'
   24 |   std::cout << "S::f: " << which<&S::f>::desc << std::endl;
      |                                        ^~
memptr.cc:6:8: note: candidates are: 'template<class K, class V, K V::* F> struct which<F> [with K = int(); V = S; K V::* F = &S::f]'
    6 | struct which<F> {
      |        ^~~~~~~~
memptr.cc:11:8: note:                 'template<class K, class V, K (V::* F)()> struct which<F> [with K = int; V = S; K (V::* F)() = &S::f]'
   11 | struct which<F> {
      |        ^~~~~~~~
memptr.cc:24:42: error: incomplete type 'which<&S::f>' used in nested name specifier
   24 |   std::cout << "S::f: " << which<&S::f>::desc << std::endl;
      |                                          ^~~~

Is this a bug in gcc, or a bug in my code? Either way, what is the simplest workaround?

JaMiT
  • 14,422
  • 4
  • 15
  • 31
user3188445
  • 4,062
  • 16
  • 26
  • 1
    I can't speak as to whether this is a bug or not -- though it appears MSVC seems to agree with clang that this compiles, which implies that it's a GCC regression. As for a potential workaround: is the example in your question representative of your actual code? Some simple workarounds could be to not do template specialization, and to instead use `if constexpr` -- should the structure lend itself to it. – Human-Compiler May 21 '21 at 22:46
  • In C++20 you can use a custom concept like `no_function` to prohibit GCC from matching it with something like `int()`. – 2b-t May 22 '21 at 00:52
  • You are allowed to answer your own question, but all answers should be posted as answers, not edited into the question. (Also, you've already indicated approval of cigien's answer by marking it as the accepted answer.) – JaMiT May 22 '21 at 01:50

3 Answers3

5

I'm not sure, but I suspect this is a GCC bug. As a workaround, you can modify your code a bit by writing the signature of F in the argument-list of the specializations instead of the parameter-list, and deducing a type instead of a non-type

template<typename F> struct which;

template<typename K, typename V>
struct which<K V::*> {
  static constexpr char desc[] = "pointer to data member";
};

template<typename K, typename V>
struct which<K (V::*)()> {
  static constexpr char desc[] = "pointer to member function";
};

To use it, you need to write which<decltype(&S::i)>::desc since a type is needed.

Demo


If you want the actual pointer to be passed to the specializations instead of a type, you could also do the following, by letting the work be done by an existing type trait

// implementation
template<auto, bool> struct which_impl;

template<auto F>
struct which_impl<F, true> {
  static constexpr char desc[] = "pointer to data member";
};

template<auto F>
struct which_impl<F, false> {
  static constexpr char desc[] = "pointer to member function";
};

// helper alias
template<auto F> 
using which = which_impl<F, std::is_member_object_pointer_v<decltype(F)>>;

Demo

cigien
  • 57,834
  • 11
  • 73
  • 112
  • This seems like a good workaround. The original `auto`-param based `which` can even be made out of this, if you rename your `which` to `which_impl`, and implement a `which` in terms of `which_impl` – Human-Compiler May 21 '21 at 23:01
  • Unfortunately, this doesn't work for me, because I need the actual pointer, not type type of the pointer. What I'm showing is just a very simple minimal non-working example, but the real code uses `F` to access values. – user3188445 May 21 '21 at 23:05
  • @user3188445 If you're actually accessing the values, are you able to just use `std::invoke` to do this more generically on the `auto` argument -- without any specialization? `std::invoke` works with both pointer-to-members and pointer-to-member-functions; it just treats the former like a 1-argument invocable expression using a pointer or reference to the class as the argument. – Human-Compiler May 21 '21 at 23:09
  • @user3188445 I added a solution to allow the pointer to be passed directly, and also using an existing type trait, so the specialization is just based on a bool. – cigien May 21 '21 at 23:14
  • This may work. Not super convenient in the actual context, but at least solves the problem. The actual type trait I want seems to be `std::is_member_function_pointer`, but if that type trait is not a special built-in, I might just be able to duplicate what it does. – user3188445 May 21 '21 at 23:20
  • @user3188445 [is_member_function_pointer](https://en.cppreference.com/w/cpp/types/is_member_function_pointer) is a type_trait. My solution can use that as well, I just picked the first one in the list. BTW, neither of these constraints, i.e. needing the pointer, or preferring to match a member function pointer, are apparent from your question. The more specific you are about what you need, the better answers you'll get. – cigien May 21 '21 at 23:22
  • I think basically your answer works, except using a default value for the template argument of `bool = std::is_member_function_pointer`, so the code is backwards compatible. – user3188445 May 21 '21 at 23:27
  • No, I take it back, somehow I'm having a hard time making the default template argument work, even with clang++. Maybe I'm doing something wrong--I'll keep at it. – user3188445 May 21 '21 at 23:30
  • [Do you need it?](https://wandbox.org/permlink/NTZD4Lnz9qBEinnC). – Paul Sanders May 21 '21 at 23:34
2

Using @cigien's answer plus default template arguments, the following seems to work around what is probably a gcc bug in a backwards-compatible way:

template<auto F,
     bool = std::is_member_function_pointer_v<decltype(F)>> struct which;

template<typename K, typename V, K V::*F>
struct which<F, false> {
  static constexpr char desc[] = "pointer to data member";
};

template<typename K, typename V, K (V::*F)()>
struct which<F, true> {
  static constexpr char desc[] = "pointer to member function";
};
user3188445
  • 4,062
  • 16
  • 26
1

In C++20 you could use concepts to make sure that the compiler does not match something like K = int() (like GCC does) by defining a concept such as

template <class T>
concept no_function = !std::is_function_v<T>;

and then enforcing it for the template parameter K

template<no_function K, typename V, K V::*F>
struct which<F> {
  static constexpr char desc[] = "pointer to data member";
};

template<no_function K, typename V, K (V::*F)()>
struct which<F> {
  static constexpr char desc[] = "pointer to member function";
};

Try it here!

2b-t
  • 2,414
  • 1
  • 10
  • 18