Making (virtual) functions made innaccessible in intermediate base class accessible in most-derived class, well-defined?

Viewed 108

Consider the following code:

#include <iostream>

struct a {
    virtual void f() = 0;
};

struct b: a {
private:
    void f() override {
        std::cout << "b::f()\n";
    }
};

struct c: b {
public:
    using a::f;
};

int main() {
    ::c c;
    c.f();
}

This compiles and works as expected with g++, clang and msvc, i.e., print b::f().

However, if I replace a and b by this:

struct a {
    void f() { }
};

struct b: a {
private:
    using a::f;
};

...the code does not compile anymore with gcc but compiles fines with both clang, clang-cl and msvc (thanks StoryTeller and Adrian Mole). I get the following error (on the using a::f; line in c):

error: 'void a::f()' is inaccessible within this context

I cannot find a clear point in the standard about the behavior of using a::f; (in c) in these cases, so are the above well-defined by the standard?

Note: I am not talking about bringing something in the public scope of a class (like using b::f in c if b::f was protected), but really making members from the top-level class accessible in the most-derived class after these have been made inaccessible in an intermediate base class.

1 Answers

I believe GCC is wrong to reject the modified code.

[namespace.udecl]

1 Each using-declarator in a using-declaration introduces a set of declarations into the declarative region in which the using-declaration appears. The set of declarations introduced by the using-declarator is found by performing qualified name lookup ([basic.lookup.qual], [class.member.lookup]) for the name in the using-declarator, excluding functions that are hidden as described below.

3 In a using-declaration used as a member-declaration, each using-declarator's nested-name-specifier shall name a base class of the class being defined. If a using-declarator names a constructor, its nested-name-specifier shall name a direct base class of the class being defined.

So first I'd note that paragraph 3 makes a distinction between a base and a direct base. Therefore we can name a::f in a using declaration. Secondly, according to paragraph 1, name lookup proceeds as one would expect

[class.qual]

1 If the nested-name-specifier of a qualified-id nominates a class, the name specified after the nested-name-specifier is looked up in the scope of the class ([class.member.lookup]), except for the cases listed below. The name shall represent one or more members of that class or of one of its base classes (Clause [class.derived]).

[class.member.lookup]

1 Member name lookup determines the meaning of a name (id-expression) in a class scope. Name lookup can result in an ambiguity, in which case the program is ill-formed. For an id-expression, name lookup begins in the class scope of this; for a qualified-id, name lookup begins in the scope of the nested-name-specifier. Name lookup takes place before access control.

So a::f is to be looked up only in the scope of a or its own base classes. It should not be looked up at all in b. I would argue that therefore the accessibility of the name f in b should not affect the accessibility of the name f in a when doing qualified name lookup.

In a, f is public. And so can be named by a qualified-id in any declarative region where a may be named. That includes c. And so the using declaration uses a valid name for a valid member of a base class. That is accessible in that declarative region. It is therefore valid.

As another data point, GCC has no problem with the accessibility of a::f in other uses. For example, GCC allows forming a pointer to member to a::f inside the scope of c.

struct c: b {
public:
    c() {
        [[maybe_unused]] auto f = &a::f;
    };
};

So it clearly does not consider the name a::f inaccessible in all contexts due to b.

Related