GCC error when using parent class method as derived class method
Asked Answered
J

2

7

I have a function in my code which only accepts a class member method as a template parameter. I need to call this method using a class method which is inherited from a parent class. Here is an example code of my problem:

template <class C>
class Test {
public:
    template<typename R, R( C::* TMethod )()> // only a member function should be accepted here
    void test() {} 
};

class A {
    public:
    int a() { return 0; } // dummy method for inheritance
};

class B : public A {
public:
    using A::a; // A::a should be declared in the B class declaration region

    // int a() { return A::a(); } // if this lines is activated compliation works
};

int main() {
    auto t = Test<B>();

    t.test<int, &B::a>();
}

With the MSVC 2019 compiler the code compiles without problems. However the gcc produces following error:

<source>: In function 'int main()':
<source>:23:23: error: no matching function for call to 'Test<B>::test<int, &A::a>()'
   23 |     t.test<int, &B::a>();
      |     ~~~~~~~~~~~~~~~~~~^~
<source>:5:10: note: candidate: 'template<class R, R (B::* TMethod)()> void Test<C>::test() [with R (C::* TMethod)() = R; C = B]'
    5 |     void test() {}
      |          ^~~~
<source>:5:10: note:   template argument deduction/substitution failed:
<source>:23:17: error: could not convert template argument '&A::a' from 'int (A::*)()' to 'int (B::*)()'
   23 |     t.test<int, &B::a>();
      |    

As far as I understand the gcc is still handling the type of B::a as A::a. On the cpp reference its saying that using

Introduces a name that is defined elsewhere into the declarative region where this using-declaration appears.

So in my opinion the using should transfer the A::a method to the declerativ region of B and therefor it should be handled as B::a. Am I wrong or is there a bug in GCC?

Here is the example on Compiler Explorer: https://godbolt.org/z/TTrd189sW

Javed answered 4/5, 2022 at 11:10 Comment(5)
It should be noted that Clang also fails to compile the code (with a much less usable error message).Boulware
"With the MSVC 2019 compiler the code compiles without problems" - MSVC versions on godbolt compile with no issues, but my local compiler 19.29.30143 does issue an error: Error (active) E0304 no function template instance "Test<C>::test [with C=B]" matches argument list.Gambetta
Note that it is not a lookup problem, it is a type problem. The name "a" is introduced in B, but the function does not become a member of B. Its type remains the same, since it refers to a member of A.Finnell
That is, using A::a; does not mean "make A::a a member of B as well", it means "use B::a as a synonym for A::a".Finnell
This would make sense. Do you have any additional reference to the C++ standard where this is explained?Javed
F
1

There is namespace.udecl, item 12 (emphasis mine):

For the purpose of forming a set of candidates during overload resolution, the functions named by a using-declaration in a derived class are treated as though they were direct members of the derived class. [...] This has no effect on the type of the function, and in all other respects the function remains part of the base class.

Thus, a is not a member of B, and the type of &B::a is int (A::*)().
(&B::a means the same thing regardless of whether you include using A::a; or not)

There is no point to using named functions from a base class except to work around the "hiding problem" when you want to overload or override them.

Finnell answered 4/5, 2022 at 13:34 Comment(0)
B
0

(Non-nullptr) pointer-to-member conversions are not allowed in converted constant expressions

So in my opinion the using should transfer the A::a method to the declerative region of B and therefor it should be handled as B::a. Am I wrong or is there a bug in GCC?

You are wrong, but we'll need to go a bit down the language rules rabbit hole to find out why.

First of all the type of the pointer to member, even when referred to via the derived class (even when introduced via a using declaration), is that of pointer-to-member-of-base. The (non-normative) example of [expr.unary.op]/3 explicitly covers this use case:

The result of the unary & operator is a pointer to its operand.

  • (3.1) If the operand is a qualified-id naming a non-static or variant member m of some class C with type T, the result has type “pointer to member of class C of type T” and is a prvalue designating C​::​m.
  • [...]

[Example 1:

struct A { int i; };
struct B : A { };
... &B::i ...       // has type int A​::​*  <-- !!!
int a;
int* p1 = &a;
int* p2 = p1 + 1;   // defined behavior
bool b = p2 > p1;   // defined behavior, with value true

— end example]

However [conv.mem]/2 covers that you may convert int (A::*)() (base) to int (B::*)() (derived):

A prvalue of type “pointer to member of B of type cv T”, where B is a class type, can be converted to a prvalue of type “pointer to member of D of type cv T”, where D is a complete class derived ([class.derived]) from B. If B is an inaccessible ([class.access]), ambiguous ([class.member.lookup]), or virtual ([class.mi]) base class of D, or a base class of a virtual base class of D, a program that necessitates this conversion is ill-formed. The result of the conversion refers to the same member as the pointer to member before the conversion took place, but it refers to the base class member as if it were a member of the derived class. The result refers to the member in D's instance of B. Since the result has type “pointer to member of D of type cv T”, indirection through it with a D object is valid. The result is the same as if indirecting through the pointer to member of B with the B subobject of D. The null member pointer value is converted to the null member pointer value of the destination type.

Namely, a pointer to member of a base class may be converted to member of derived class, and indeed, the following program, where conversion is made in the context of an argument (pointer-to-member of base) to a function parameter (type: pointer-to-member of derived) is well-formed:

struct A {
    int a() { return 0; };
};

struct B : A {};

void f(int( B::*)()) {}

int main() {
    f(&A::a);  // OK: [conv.mem]/2
}

Then why does the case of a template parameter fails? A more minimal example is:

struct A {
    int a() { return 0; };
};

struct B : A {};

template<int(B::* TMethod )()>
void g() {}

int main() {
    g<&A::a>();  // error
}

The root cause is that template argument deduction fails: the template argument is &A::a of type int(A::*)() and [temp.arg.nontype]/2 applies:

A template-argument for a non-type template-parameter shall be a converted constant expression ([expr.const]) of the type of the template-parameter.

A (non-nullptr) pointer-to-member conversion ([conv.mem]/2) is not allowed in a converted constant expression (refer to [expr.const]/10), meaning &A::a is not a valid template argument to a non-type template parameter of type int(B::*)().

We may note that Clang actually gives us a very clear diagnostic on this if we change to class template:

struct A {
    int a() { return 0; };
};

struct B : A {};

template<int(B::*)()>
struct C {};

int main() {
    C<&A::a> c{};
    // error: conversion from 'int (A::*)()' to 'int (B::*)()' 
    //        is not allowed in a converted constant expression
}
Blackfish answered 4/5, 2022 at 11:53 Comment(2)
This seems to be missing the point. If int (A::*p)(); p= &B::a; compiles just fine (and it should), then I don't see what the problem is, with the template.Lloyd
@SamVarshavchik Updated with a few more standard references: I think the key here is that a pointer-to-member conversion (as per [conv.mem]) is not a converted constant expression, meaning [temp.arg.nontype]/2 does not apply, and template argument deduction fails.Blackfish

© 2022 - 2024 — McMap. All rights reserved.