Search code examples
c++templatesinner-classesprivate-members

Why is using a private inner class as parameter an error in the header in one case, and in the calling function in the other?


I'm trying to build a templated C++ function that accepts, as its argument, a pointer to an object of an inner class. (An earlier difficulty I had on this problem can be seen in "How to make C++ find templated function header when dependent-scoped inner classes are involved?".) At one point I was having a problem that turned out to be an access control issue; I've figured out how to avoid the problem, but I'm still sort of shaky on why this problem was reported in the way it was (which sent me down a garden path in terms of tracking down the problem).

Basically, I was trying to call the function, with a private-inner-class object as a parameter, from a method inside a friend class (which therefore possesses the required access privilege). The problem, of course, was that the non-method function was not a friend and so didn't have access to the inner class. But the compiler reported it as a header mismatch rather than an access problem.

First, the setup. My data class has the following general structure:

template <typename T>
class Outer
{
  private:
  struct Inner
  {
    T val;
    Inner (T v) : val(v) { }
  };

  public:
  Inner* ptr = nullptr;

  friend class Other;
};

If I write a simple function that is not templated and tries to take an Inner* as a parameter, the compiler correctly diagnoses the problem:

void testUntemplated (const Outer<int>::Inner* p)
{
  cout << p->val << endl;
}

Without even calling this function, as soon as it is defined, g++ tells me error: ‘struct Outer<int>::Inner’ is private, and clang++ tells me error: 'Inner' is a private member of 'Outer<int>'. Super-duper.

But now consider this templated function, called from within a class:

template <typename T>
void testFunction (const typename Outer<T>::Inner* p)
{
  cout << p->val << endl;
}

struct Other {
  template<typename T>
  static void main(const Outer<T>& foo)
  {
    testFunction<T>(foo.ptr);  //this line is flagged as the error
  }
};

Even though Outer is already defined, and Inner is private---regardless of the typename that is used to fill in for T---the function definition passes through without error; in this case it is the call to testFunction that is flagged, with the singularly unhelpful message error: no matching function for call to ‘testFunction(Outer<int>::Inner* const&)’ in g++. Happily, clang++ does a bit better: it flags the same line with an error (error: no matching function for call to 'testFunction') but follows this up with an explanatory note (note: candidate template ignored: substitution failure [with T = int]: 'Inner' is a private member of 'Outer<int>').

But even though the more-helpful clang++ has found what I see as the actual source of error here (the defined function doesn't have access to the type of its parameter), it still says the error lies in the call, not the definition, and I've learned that this sort of mismatch usually means I've found some corner of C++ that I don't understand as well as I should (especially when different compilers have the same seemingly-weird error behaviour). What actually is going on here? Why can't it reject the templated function in exactly the same way that it rejects the regular function?


Solution

  • Consider this code, which is perfectly legal C++ and compiles and runs fine:

    #include <iostream>
    
    template <typename T>
    class Outer
    {
    private:
        struct Inner { };
    };
    
    template <typename T>
    void testFunction (const typename Outer<T>::Inner* p)
    {
        std::cout << "Hello!" << std::endl;
    }
    
    template<>
    class Outer<double>{
    public:
        struct Inner { };
    };
    
    int main()
    {
        Outer<double>::Inner i;
        testFunction<double>(&i);
        return 0;
    }
    

    The compiler can't issue a diagnostic at Outer<T>::Inner because it doesn't know if there's going to be an explicit specialization of Outer later on that makes the function well-formed. When you later attempt to call testFunction<int>, SFINAE kicks in to remove the ill-formed function signature (because Inner is private) from the overload set, giving you a "no matching function" error.

    However, when you write Outer<int>::Inner in your function definition (as in your testUntemplated), that's going to trigger an implicit instantiation unless there's an explicit specialization for Outer<int> previously declared. The compiler need not worry about later explicit specializations because the standard requires explicit specializations to be declared before anything that can trigger an implicit instantiation (§14.7.3 [temp.expl.spec]/p6):

    If a template, a member template or a member of a class template is explicitly specialized then that specialization shall be declared before the first use of that specialization that would cause an implicit instantiation to take place, in every translation unit in which such a use occurs; no diagnostic is required.

    Finally, note that compilers are not required to generate a diagnostic for a template with no valid specialization if it is never instantiated (§14.6 [temp.res]/p8):

    If no valid specialization can be generated for a template, and that template is not instantiated, the template is ill-formed, no diagnostic required.