8

Suppose I have the following template class that defines a nested class:

template <typename T>
struct foo {
    struct bar { };
};

Suppose the environment I'm coding in also has the following helper class, which should be specialized for any type that needs special handling:

template <typename T>
struct maybeChangeType { using type = T; }  /* default: same type */

How can I specialize maybeChangeType for foo<T>::bar? It's easy enough to specialize for, say, foo<int>::bar, but foo will be used with 100+ different T so that's not really an option.

NOTE: Please read carefully before marking this question as a duplicate. This question is not asking how to specialize in general (e.g. Understanding templates in c++), or how to declare friends, or even how to declare friends of templates. It is asking how to declare a friend for a non-template nested member of a template class (as the title states).

Trying to define specializations in the "normal" way does not work because foo<T>::bar is not a deducible context (bad sign: it needs typename in front):

/* error: template parameters not deducible in partial specialization */
template <typename T>
struct maybeChangeType<typename foo<T>::bar>;

Declaring the specialization as a friend also produces compilation errors:

template <typename T>
struct foo {
    struct bar {
        /* errors:
         * - class specialization must appear at namespace scope
         * - class definition may not be declared a friend
         */
        template <>
        friend struct maybeChangeType<bar> { using type=T; };
    };
};

The above errors make it clear that the actual definition of these friends has to be out of line:

template <typename T>
struct foo {
    struct bar {
        friend struct maybeChangeType<bar>;
    };
};

But now we're back where we started: any attempt to define a specialization for foo<T>::bar will fail because it uses bar in a non-deducible context.

NOTE: I can work around the issue for functions by providing a friend overload inline, but that's no help for a class.

NOTE: I could work around the issue by moving the inner class out to namespace scope, but that would pollute the namespace significantly (lots of inner classes the user really has no business playing with) and complicate the implementation (e.g. they would no longer have access to private members of their enclosing class and the number of friend declarations would proliferate).

NOTE: I understand why it would be dangerous/undesirable to allow arbitrary specialization of the name foo<T>::bar (what if foo<T> had using bar = T for example), but in this case bar really is a class (not even a template!) which foo really does define, so there shouldn't be any ODR hairiness or risk that the specialization would affect other (unintended) types.

Ideas?

Community
  • 1
  • 1
Ryan
  • 731
  • 6
  • 17

3 Answers3

3

As an intrusive solution, it is possible to use functions for type programming (metaprogramming). You could write the type function as a friend function:

template<typename T> struct type_t { using type = T; };

template <typename T>
struct foo {
    struct bar {
        friend constexpr auto maybeChangeType_adl(type_t<bar>) -> type_t<T>
        { return {}; }
    };
};

Replace type_t<T> with any type_t<my_type_function_result>. It is not necessary, but sometimes convenient, to define this function, e.g. for computations in constant expressions. type_t can be enhanced with comparison operators, to replace std::is_same<A, B> with infix a == b for example. The type type_t<T> is used instead of T directly for two reasons:

  1. For a definition of maybeChangeType_adl, it is necessary to construct an object of the return type.
  2. Not all types can be returned from a function. E.g. abstract types, function types, array types. Those can still be used as template arguments.

In C++14, I'd use variable templates to implement this function:

template<typename T> constexpr auto type = type_t<T>{};

// ...

friend constexpr auto maybeChangeType_adl(type_t<bar>) { return type<T>; };

Though this loses some of the symmetry (parameter vs return type).


In any case, you can query the type as follows:

template<typename T> using inner_type = typename T::type;

template<typename T> using maybeChangeType =
    inner_type<decltype(maybeChangeType_adl(type_t<T>{}))>;

A fall-back function maybeChangeType can be provided to mirror the primary template in the OP:

template<typename T> auto maybeChangeType(type_t<T>) -> type_t<T> { return {}; }

Or, you specialize a maybeChangeType class template on the existence of the maybeChangeType_adl function:

template<typename T, typename = void>
struct maybeChangeType { using type = T; };

template<typename T>
struct maybeChangeType<T, void_t<decltype(maybeChangeType_adl(type_t<T>{}))>>
{ using type = inner_type<decltype(maybeChangeType_adl(type_t<T>{}))>; };
dyp
  • 38,334
  • 13
  • 112
  • 177
1

If you can can export the template parameter T in bar, and if you can add another (defaulted) template parameter to maybeChangeType, you can try this:

#include <type_traits>

template <typename T>
struct foo {
    struct bar {
      using type = T;
    };
};

template <typename T, typename = void>
struct maybeChangeType { using type = T; };  /* default: same type */

template <typename T>
struct maybeChangeType<T,
    typename std::enable_if<std::is_same<T,
                                         typename foo<typename T::type>::bar
                                         >::value
                            >::type>
{ using type = T; };

int main() {}
ex-bart
  • 1,352
  • 8
  • 9
  • The `using type = T` in my above example was just for brevity. In real life, it would be `using type = AnotherNestedClass`. Tho maybe I could use SFINAE to try and pull the typename out of the class... let me play with that. – Ryan Aug 21 '15 at 16:34
  • If you can recognize instances of `bar` by some member type they contain, you can use `AlwaysTrue::value` as the `enable_if` condition, where `template struct AlwaysTrue : std::true_type {};`. – ex-bart Aug 21 '15 at 16:39
  • But how do I differentiate the enable_if specialized version from the default without exposing users to this sort of ickiness? – Ryan Aug 21 '15 at 16:56
  • @ex-bart I'd suggest using a unique type identifier instead; something like `struct foo_bar {};` at namespace scope, then `using identifier = foo_bar;` within each `bar`, and query `enable_if_t::value>` – dyp Aug 21 '15 at 16:56
  • @ex-bart I don't think this actually works. It compiles, but it uses foo in a non-deducible context and so is never selected. – Ryan Aug 21 '15 at 18:10
  • @Ryan I'm not seeing anything above that should not work. Quite possibly there is a typo or inconsistency, but the question is a moving target, so that isn't surprising. – Yakk - Adam Nevraumont Aug 21 '15 at 18:35
  • 1
    @Ryan I tried a few variations [here](http://coliru.stacked-crooked.com/a/fbf7195139dd1fe7). All worked with g++ 5.2.0 (as well as with g++ 4.9.2 on my machine), except when I tried to define `AlwaysTrue` as a type alias instead of as a derived class. What compiler are you using? – ex-bart Aug 21 '15 at 19:33
0

Inspired by the answers from @ex-bart and @dyp, I was able to come up with the following C++11 solution:

struct Helper {
    template <typename T>
    static typename T::other_type getType(T);

    template <typename T, typename... Ignored>
    static T getType(T, Ignored...);
};

template <typename T>
struct maybeChangeType {
    using type = decltype(Helper::getType(std::declval<T>()));
};

struct foo { };
struct bar { using other_type = int; }

int main() {
    maybeChangeType<foo>::type f = foo();
    maybeChangeType<bar>::type b = int();
}

It has the advantage of not exposing end users or class implementations to the black magic---users just use maybeChangeType and classes can specialize it by simply providing a typedef other_type.

Ryan
  • 731
  • 6
  • 17
  • This requires that every type supported be put in one spot -- in `Helper` -- and as such doesn't scale well. ADL, or SFINAE, based solutions let the support be distributed if needed. – Yakk - Adam Nevraumont Aug 21 '15 at 18:30
  • @Yakk I don't think I follow. If `T` contains the class or typedef `other_type` then the above will pick it up. If not, `T` will be used instead. As long as T is defined before maybeChangeType is used, it should work. And it is impossible to use maybeChangeType with an incomplete type because std::declval blows up. – Ryan Aug 21 '15 at 19:45
  • suppose you had a `template struct bob {};` which does not have an `other_type` alias, and you want `maybeChangeType` to return `T` if `T` is an integral type. With the above system, any such improvements need be done within `struct Helper` or via modification of `maybeChangeType`. You do get "detect `other_type` and use it if present", but that's about it. And there are cleaner ways to do that! – Yakk - Adam Nevraumont Aug 21 '15 at 20:33
  • I'm always interested in cleaner ways of doing things, but could you elaborate? Nothing presented here so far strikes me as a "clean" solution. For the bob situation you describe, I would just specialize the template---no mess, no fuss. Works great for std::array, std::pair, etc. In fact, that's what I was doing with all my code until I realized that I couldn't specialize for classes nested inside template classes... – Ryan Aug 22 '15 at 02:20
  • [can_apply](http://stackoverflow.com/questions/30189926/metaprograming-failure-of-function-definition-defines-a-separate-function/30195655#30195655) or the equivalent std::experimental version. – Yakk - Adam Nevraumont Aug 22 '15 at 02:22