4

I have a base class defining a constant and the child class can use it using alias. The construct is as below

class Base
{
protected:
    static const int A_ = 1;
};

class Foo : public Base
{
private:
    using Base::A_;
};

However, when I define a subclass of Foo as

class Go : public Foo
{
private:
    using Base::A_;
};

the compiler emits the error: error: ‘const int Base::A_’ is private within this context. I do not get it since Base::A_ is protected. What did the compiler see in this case and what can be the solution to use Base::A_ in Go ?

8
  • The compiler sees the private A_ from Foo
    – RoQuOTriX
    Commented Jan 17, 2023 at 9:23
  • why is it not protected in Foo ? Commented Jan 17, 2023 at 9:25
  • 1
    btw its not an alias. Here using just makes the name accessible elsewhere. Commented Jan 17, 2023 at 9:29
  • @463035818_is_not_a_number the intention is to not make Foo::A_ accessible to avoid confusion.
    – kstn
    Commented Jan 17, 2023 at 9:36
  • but your intention is to access A_ in the class derived from Foo. Thats what protected is for. The access specifiers are not transitive. Commented Jan 17, 2023 at 9:38

2 Answers 2

0

I do not get it since Base::A_ is protected.

Go inherits from Foo not from Base. A_ is private in Foo not protected.

If you want to have access to A_ in classes derived from Foo then A_ should be public or protected in Foo.

4
  • 1
    Seems not as trivial. msvc and clang accept the original code. Demo
    – Jarod42
    Commented Jan 17, 2023 at 15:33
  • @Jarod42 I admit, I wasnt completely certain about the answer. cppreference only talks about the direct base (en.cppreference.com/w/cpp/language/using_declaration). I'm afraid a language lawyer is needed Commented Jan 17, 2023 at 15:39
  • @463035818_is_not_a_number so this is yet to be defined in the standard and compiler-dependent?
    – kstn
    Commented Jan 18, 2023 at 8:14
  • @kstn I need to do more research. For now it would be cool if you could unaccept the answer. Commented Jan 18, 2023 at 8:43
0

tl;dr

  • From a strict standards perspective none of the 3 compilers are right.
  • Your example should be ill-formed in C++20 (and previous versions), because it violates 9.9 [namespace.udecl] (17):

    (17) [...] The base class members mentioned by a using-declarator shall be visible in the scope of at least one of the direct base classes of the class where the using-declarator is specified.

    • The commitee also confirmed this rationale in CWG 1960:

      Rationale (November, 2014):
      The rule was introduced because the hiding of a base class member by an intermediate derived class is potentially intentional and should not be capable of circumvention by a using-declaration in a derived class. The consensus of CWG preferred not to change the restriction.

  • ... but in practice none of the 3 major compilers (gcc, clang, msvc) implements this correctly.
  • This will most likely be fixed in C++23; The cited paragraph from above will be removed by P1787, thereby making your example well-formed.
    • The paper also mentions the sad reality of current implementations: P1787 R6

      CWG1960 (currently closed as NAD) is resolved by removing the rule in question (which is widely ignored by implementations and gives subtle interactions between using-declarations).

  • gcc has a name-resolution bug that causes your compiler error: gcc Bug 19377

1. Disclaimer

  • This post refers only to the C++20 standard; i haven't checked all relevant sections in all previous standards.
  • In my examples i'll use both static and non-static data members; the rules regarding using-declarations are the same for both. 1

  1. This does not extend to other class member declarations; notably nested class and enumeration type members (class.qual 1.4) and constructors (class.qual 2.2, namespace.udecl) have a few special rules.


2. Long Explanation

The main rule we need to consider to check if your code is well formed is:
9.9 The using declaration [namespace.udecl] (17)

(17) In a using-declarator [...] all members of the set of introduced declarations shall be accessible. [...] In particular, if a derived class uses a using-declarator to access a member of a base class, the member name shall be accessible. [...] The base class members mentioned by a using-declarator shall be visible in the scope of at least one of the direct base classes of the class where the using-declarator is specified.

So there are two checks that a member using-declarator must pass for it to be well-formed:

  • The member it refers to must be accessible
  • The member it refers to must be visible within a direct base class

2.1 Pre-reqs

There are a few key concepts we need to cover before we can get to performing those two checks:

2.1.1 Visibility and Accessibility

I'm going to assume familiarity with visibility and accessibility.
Here's a quick definition if required:

  • Accessiblity refers to private, protected, public access control
  • Visibility refers to name hiding, e.g. in the following example the outer x variable is hidden by the inner x variable (so within the inner scope the outer x variable is NOT visible):
    {
        int x;
        {
            int x;
        }
    }
    

Here's a godbolt that demonstrates all 4 possible combinations of accessibility and visibility for class members.

2.1.2 using-declarators hide members of base classes

Another key concept we need to cover is that using-declarators introduce synonyms - those synonyms are member-declarations and as such can hide members of the same name from base classes.

This is given by 9.9 The using declaration [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 unqualified-id of the] using-declarator is declared in the declarative region in which the using-declaration appears as a synonym for each declaration introduced by the using-declarator. [...]
(2) Every using-declaration is a declaration and a member-declaration and can therefore be used in a class definition.

and 6.4.10 Name hiding [basic.scope.hiding]:

(1) A declaration of a name in a nested declarative region hides a declaration of the same name in an enclosing declarative region.

This basically means that using Base::A_ within Foo will create a synonym - Foo::A_ - that refers to Base::A_.
This synonym hides the original Base::A_ member.

Note that the accessibility of Base::A_ has not been changed - it is still public - Base::A_ is just not visible anymore because the synonym Foo::A_ hides it within Foo and Go.

Here's a small example similar to your example: godbolt

struct Base {
    static const int A_ = 1;
};

struct Foo : Base {
private:
    // this introduces a synonym for Base::A_ which hides Base::A_
    // (only the synonym is visible within Foo and Go)
    using Base::A_;
};

struct Go : Foo {
    int go();
};

In this case the following applies:
Within the scope of Go:

  • Base::A_ is NOT visible, but it is accessible (hidden by Foo::A_)
  • Foo::A_ (the synonym) is visible, but it is NOT accessible

Note that we can't access A_ within Go directly because it would resolve to Foo::A_ which is not accessible:

int Go::go() {
    // resolves to the synonym Foo::A_, which is private in Foo
    // -> ill-formed
    return A_;
}

But we can still access Base::_A from within Go (it is accessible, just not visible) by using a nested-name-specifier: 2

int Go::go() {
    // resolves directly to Base::A_, which is public in Base
    // -> well-formed
    return Base::A_;
}

  1. This is possible because nested-name-specifiers only consider the specified class scope, i.e. in this case it'll only look for A_ in Base, but NOT in Foo. There's also a mention of this in class.qual.


2.2 So is your example well-formed?

So lets get back to actually checking if your code example is well-formed:

  • The member it refers to must be accessible
  • The member it refers to must be visible within a direct base class
2.2.1 Is Base::A_ accessible?

using-declarations are resolved using qualified name lookup, so the name must be resolved within the given class.

9.9 The using declaration[namespace.udecl]

1 [...] The set of declarations introduced by the using-declarator is found by performing qualified name lookup for the name in the using-declarator. [...]

In your case this means that using Base::A_; within Go must only consider members within Base for this check - NOT within Foo.

  • Base::A_ is a public member
  • Go inherits publically from Foo, which in turn inherits publically from Base

So Base::A_ must be accessible within Go.

Note that this is where gcc messes up - instead of only considering members of Base it also checks in Foo and finds the synonym, which produces the bogus compiler error about Foo::A_ not being accessible.

This is most likely related to gcc Bug 19377.

gcc also doesn't like (some) nested name specifiers; note that if we use the example from above but use a non-static data member then gcc suddenly doesn't like the nested name specifier:

godbolt

struct Base {
    int A_ = 1;
};

struct Foo : Base {
private:
    using Base::A_;
};

struct Go : Foo {
    int go() {
        // resolves to the synonym Foo::A_ (which is private)
        // -> ill-formed
        // all 3 compilers agree
        return A_;

        // nested-name-specifier that resolves directly to Base::A_
        // -> well-formed
        // (only gcc wrongly rejects this)
        return Base::A_;

        // same as above, just with explicit this
        // -> well-formed
        // all 3 compilers agree (even gcc, hooray!)
        return (*this).Base::A_;
        return this->Base::A_;
    }
};
2.2.2 Is Base::A_ visible within a direct base class?

Note that is calls for a check of visibility within a direct base class - in your case Go only inherits from Foo, so Foo is the only direct base class of Go.

As covered above Base::A_ is NOT visible within Foo (it gets hidden by the synonym Foo::A_), so this check should fail (and therefore your example should be ill-formed)

However neither gcc, clang nor msvc actually do this check (even though it is clearly mandated by the standard).

So none of those 3 compilers are conforming to the standard in this case.

This has also been covered in a standard defect report: CWG 1960

The base class members mentioned by a using-declaration shall be visible in the scope of at least one of the direct base classes of the class where the using-declaration is specified.

The rationale for this restriction is not clear and should be reconsidered.

Rationale (November, 2014): The rule was introduced because the hiding of a base class member by an intermediate derived class is potentially intentional and should not be capable of circumvention by a using-declaration in a derived class. The consensus of CWG preferred not to change the restriction.

which has been resolved in 2014 as NAD (not a defect), i.e. clearly indicating that the standard committee wants your example to be ill-formed.

In case you're interested those are the open compiler bugs in gcc and clang for this: (unresolved for quite some time now)


3. C++23 to the rescue!

The paper P1787R6 will remove the visibility check completely from that paragraph:

(17) In a using-declarator that does not name a constructor, all members of the set of introduced [every] declaration[s] [named] shall be accessible. > In a using-declarator that names a constructor, no access check is performed.
In particular, if a derived class uses a using-declarator to access a member of a base class, the member name shall be accessible. If the name is that of an overloaded member function, then all functions named shall be accessible. The base class members mentioned by a using-declarator shall be visible in the scope of at least one of the direct base classes of the class where the using-declarator is specified.

P1787R6 will most likely be merged into the C++ Standard with C++23.
So in C++23 your example should be well-formed out of the box.

There's even a rationale for the change in that paper:

CWG1960 (currently closed as NAD) is resolved by removing the rule in question (which is widely ignored by implementations and gives subtle interactions between using-declarations).


4. Potential fix for C++ Versions before C++23

A potential way in which you could make your code well-formed before C++23 would be to make Base a direct base class of Go.

A cheesy way you could accomplish this would be by using virtual inheritance: godbolt

struct Base {
    static const int A_ = 1;
    int B_ = 1;
};

struct Foo : virtual Base {
private:
    using Base::A_;
    using Base::B_;
};

struct Go : Foo, virtual Base {
    using Base::A_;
    using Base::B_;
};

Not the answer you're looking for? Browse other questions tagged or ask your own question.