Search code examples
c++classtemplatestypesinstantiation

Nested class of a template class - interoperability and visibility


I have a template class A<T> with a nested class Inner - I use it to hide implementation details. I might have different instantiations of the class A. A can hold a pointer to Inner and I might need to do operations with those pointers within 2 different instantiations. In those cases, I might get 2 different Inner classes - A<T1>::Inner and A<T2>::Inner. From a compiler perspective, these are different types and are not compatible. A non-working example:

template<typename T>
class A {
    template <typename T2> friend class A;

    struct Inner {
        Inner(int64_t timestamp) : timestamp(timestamp) {}
        int64_t timestamp;
    };

    Inner* inner_pointer;
    T val;

public:
    template<typename U>
    A(const A<U> other, const T& val) : inner_pointer(other.inner_pointer), val(val) {}

    A(const T& val, int64_t time_now) : inner_pointer(new Inner(time_now)), val(val) {}
};

int main(int argc, char const *argv[]){
    A first('a', 123);
    A second(first, 0.2);
    return 0;
}

One way to get around this could be to use reinterpret_cast<Inner*>(other.inner_pointer), but that's ugly, and I would guess a UB as well.

Another way is not to make Inner and nested class, but a separate class instead:

struct Inner {
    Inner(int64_t timestamp) : timestamp(timestamp) {}
    int64_t timestamp;
};

template<typename T>
class A {
    template <typename T2> friend class A;

    Inner* inner_pointer;
    T val;

public:
    template<typename U>
    A(const A<U> other, const T& val) : inner_pointer(other.inner_pointer), val(val) {}

    A(const T& val, int64_t time_now) : inner_pointer(new Inner(time_now)), val(val) {}
};

This solves the problem, but then it makes the Inner class visible to anyone who uses the A class since it is part of the header file that needs to be included and it also pollutes the scope.

I guess the STL implementations get around this using names that are reserved (starting with underscore). Is there a simple/elegant way how to achieve this without exposing the Inner class?


Solution

  • Here's a trick based on the assumption that A<void> is not a valid instantiation. (If this is an artifact of simplification, there are other ways to get a similar effect.)

    First, declare (not define) your template and define A<void> with Inner as a nested type. More generally, you could move all the common functionality, such as the inner_pointer member, to this specialization. This opens the possibility of moving the common code, including the definition of Inner, out of the header to a source file.

    #include <cstdint>
    
    // Declare the template.
    template<typename T>
    class A;
    
    // Define A<void>
    template<>
    class A<void> {
        template <typename T2> friend class A;
    
        struct Inner {
            Inner(int64_t timestamp) : timestamp(timestamp) {}
            int64_t timestamp;
        };
    
        // Private constructor prevents the use outside the A template.
        A() = default;
        // Protected (or private) destructor allows safe polymorphism without virtual functions.
        ~A() = default;
    };
    

    Next, define your general template, using A<void> as a base type.

    template<typename T>
    class A : private A<void> {
        template <typename T2> friend class A;
    
        Inner* inner_pointer;
        T val;
    
    public:
        template<typename U>
        A(const A<U> other, const T& val) : inner_pointer(other.inner_pointer), val(val) {}
    
        A(const T& val, int64_t time_now) : inner_pointer(new Inner(time_now)), val(val) {}
    };
    

    All your template instantiations now use the same Inner type.

    The drawback of this is that we have not really changed the problem; the A<void> type is visible to anyone who uses the A template. However, it is not usable (since everything is private), and we have eliminated the namespace pollution as there is not a new identifier introduced.