Why Can't the Template Argument Be Deduced When It Is Used as Template Parameter to Another Template

Why can't the template argument be deduced when it is used as template parameter to another template?

That is non-deducible context. That is why the template argument cannot be deduced by the compiler.

Just imagine if you might have specialized TMap as follows:

template <>
struct TMap<SomeType>
{
typedef std::map <double, double> Type;
};

How would the compiler deduce the type SomeType, given that TMap<SomeType>::Type is std::map<double, double>? It cannot. It's not guaranteed that the type which you use in std::map is also the type in TMap. The compiler cannot make this dangerous assumption. There may not any relation between the type arguments, whatsoever.

Also, you might have another specialization of TMap defined as:

template <>
struct TMap<OtherType>
{
typedef std::map <double, double> Type;
};

This makes the situation even worse. Now you've the following:

  • TMap<SomeType>::Type = std::map<double, double>.
  • TMap<OtherType>::Type = std::map<double, double>.

Now ask yourself: given TMap<T>::Type is std::map<double, double>, how would the compiler know whether T is SomeType or OtherType? It cannot even know how many such choices it has, neither can it know the choices themselves...

I'm just asking you for the sake of thought-experiment (assuming it can know the complete set of choices).

Template parameter cannot be deduced

This is because your case is a Non-deduced contexts.

Cited from http://en.cppreference.com/w/cpp/language/template_argument_deduction:

Non-deduced contexts

In the following cases, the types, templates, and non-type values that are used to compose P do not participate in template argument deduction, but instead use the template arguments that were either deduced elsewhere or explicitly specified. If a template parameter is used only in non-deduced contexts and is not explicitly specified, template argument deduction fails.

1) The nested-name-specifier (everything to the left of the scope resolution operator ::) of a type that was specified using a qualified-id

In your case, typename MyType_OutArg<T>::type will not participate in type deduction, and T is not known from elsewhere, thus this template function is ignored.

Visual C++ cannot deduce template template parameter

I have encountered this as well with Visual C++ and I think in this regard the Visual C++ compiler is not compliant with the C++17 standard and your code is correct (but your code won't work with an std::vector with custom allocator!). The standard containers have in fact two template parameters: The value type and the allocator (which defaults to std::allocator<T>). Prior to C++17 template template matching required the template parameters to match exactly while in C++17 this was relaxed to include default arguments as well. Yet for some reason Visual C++ seems still to expect the second template argument std::allocator<T> and not assume the given default argument.

The following sections will discuss the template template matching for the different standards in more detail. At the end of the post I will suggest alternatives that will make your code compile on all said compilers which takes the form of SFINAE with two two template arguments (so that it works with custom allocators as well) for C++17 and std::span for C++20 and onwards. std::span actually does not need any template at all.


Template parameters of std:: Containers

As pointed out in the post that you linked already standard-library containers such as std::vector, std::deque and std::list actually have more than one template parameter. The second parameter Alloc is a policy trait which describes the memory allocation and has a default value std::allocator<T>.

template<typename T, typename Alloc = std::allocator<T>>

Contrary std::array actually uses two template parameters T for the data type and std::size_t N for the container size. This means if one wants to write a function that covers all said containers one would have to turn to iterators. Only in C++20 there is a class template for contiguous sequences of objects std::span (which is sort of a super-concept that encapsulates all of the above) that relaxes this.

Template template matching and the C++ standard

When writing a function template whose template arguments themselves depend on template parameters you will have to write a so called template template function, meaning a function of the form:

template<template<typename> class T>

Note that strictly according to the standard template template parameters would have to be of declared with class not with typename prior to C++17. You could certainly somehow circumvent such a template template construct (from C++11 onwards) with a very minimal solution such as (Godbolt)

template<typename Cont>
void f (Cont const& cont) {
using T = Cont::value_type;
return;
}

which assumes that the container contains a static member variable value_type which is then used to define the underlying data type of the elements. This will work for all said std:: containers (including the std::array!) but is not very clean.

For template template function there exist particular rules which actually changed from C++14 to C++17: Prior to C++17 a template template argument had to be a template with parameters that exactly match the parameters of the template template parameter it substitutes. Default arguments such as the second template argument for the std:: containers, the aforementioned std::allocator<T>, were not considered (See the "Template template argument" section here as well as in the section "Template template arguments" on the page 317 of this working draft of the ISO norm or the final C++17 ISO norm):

To match a template template argument A to a template template
parameter P, each of the template parameters of A must match
corresponding template parameters of P exactly (until C++17) P
must be at least as specialized as A (since C++17).

Formally, a template template-parameter P is at least as specialized
as a template template argument A if, given the following rewrite to
two function templates, the function template corresponding to P is at
least as specialized as the function template corresponding to A
according to the partial ordering rules for function templates. Given
an invented class template X with the template parameter list of A
(including default arguments):

  • Each of the two function templates has the same template parameters, respectively, as P or A.
  • Each function template has a single function parameter whose type is a specialization of X with template arguments corresponding to the
    template parameters from the respective function template where, for
    each template parameter PP in the template parameter list of the
    function template, a corresponding template argument AA is formed. If
    PP declares a parameter pack, then AA is the pack expansion PP...;
    otherwise, AA is the id-expression PP.

If the rewrite produces an invalid type, then P is not at least as
specialized as A.

Therefore prior to C++17 one would have to write a template mentioning the allocator as a default value manually as follows. This works also in Visual C++ but as all the following solutions will exclude the std::array (Godbolt MSVC):

template<typename T, 
template <typename Elem,typename Alloc = std::allocator<Elem>> class Cont>
void f(Cont<T> const& cont) {
return;
}

You could achieve the same thing in C++11 also with variadic templates (so that the data-type is the first and the allocator the second template argument of the template parameter pack T) as follows (Godbolt MSVC):

template<template <typename... Elem> class Cont, typename... T>
void f (Cont<T...> const& cont) {
return;
}

Now in C++17 actually the following lines should compile and work with all std:: containers with the std::allocator<T> (See section 5.7 on pages 83-88, in particular "Template Template Matching" on page 85, of "C++ Templates: The complete guide (second edition)" by Vandevoorde et al., Godbolt GCC).

template<typename T, template <typename Elem> typename Cont>
void f (Cont<T> const& cont) {
return;
}

The quest for a generic std:: container template

Now if your goal is to use a generic container that only holds integers as template arguments and you have to guarantee that it compiles on Visual C++ as well then you have following options:

  • You could extend the minimalistic unclean version with a static_assert to make sure that you are using the correct value type (Godbolt). This should work for all kinds of allocators as well as the std::array but it is not very clean.

      template<typename Cont>
    void f (Cont const& cont) {
    using T = Cont::value_type;
    static_assert(std::is_same<T,int>::value, "Container value type must be of type 'int'");
    return;
    }
  • You could add the std::allocator<T> as a default template argument which has the disadvantage that your template then won't work if somebody uses a container with custom allocator and will neither work with std::array (Godbolt):

      template<template <typename Elem,typename Alloc = std::allocator<Elem>> class Cont>
    void f(Cont<int> const& cont) {
    return;
    }
  • Similar to your code you could specify the allocator as second template argument yourself. Again this won't work with another type of allocator (Godbolt):

      template<template <typename... Elem> class Cont>
    void f(Cont<int, std::allocator<int>> const& cont) {
    return;
    }
  • So probably the cleanest approach prior to C++20 would be to use SFINAE to SFINAE out (meaning you add a certain structure inside the template which makes the compilation file if it does not meet your requirements) all other implementations that are not using the data type int with type_traits (std::is_same from #include <type_traits>, Godbolt)

      template<typename T, typename Alloc,  
    template <typename T,typename Alloc> class Cont,
    typename std::enable_if<std::is_same<T,int>::value>::type* = nullptr>
    void f(Cont<T,Alloc> const& cont) {
    return;
    }

    or which are not integer types (std::is_integral, Godbolt) as this is much more flexible regarding the template parameter Alloc:

      template<typename T, typename Alloc, 
    template <typename T,typename Alloc> class Cont,
    typename std::enable_if<std::is_integral<int>::value>::type* = nullptr>
    void f(Cont<T,Alloc> const& cont) {
    return;
    }

    Furthermore this can be extended easily with logical or || and logical and &&. Since C++14 one might also the corresponding aliases and write std::enable_if_t<std::is_same_v<T,int>> instead of std::enable_if<std::is_same<T,int>::value>::type which makes it a little less awkward to read.

  • Finally in the newest standard C++20 you should even be able use the long-awaited concepts (#include <concepts>) using the Container concept (see also this Stackoverflow post) e.g. as follows (Wandbox)

      template<template <typename> typename Cont>
    requires Container<Cont<int>>
    void f(Cont<int> const& cont) {
    return;
    }
  • And similar in C++20 there exists std::span<T> which unlike all solutions above works with std::array as well (Wandbox)

      void f(std::span<int> const& cont) {
    return;
    }

Template parameter can't be deduced on implicitly constructed argument

would like to have do_thing({test}) or do_thing(test) implicitly construct a Bar and pass that as the argument if possible.

Unfortunately, when you call do_thing({test}) or do_thing(test), test (or {test}) isn't a Bar<T> object. So the compiler can't deduce the T type and can't construct a Bar<T> object.

A sort of chicken-and-egg problem.

The best I can imagine is to add, in Foo, a do_test() method as follows

template<typename T>
auto do_thing (T const & t)
{ return do_thing(Bar{t}); }

This way you can call (without graphs)

std::vector<std::string> s = foo.do_thing(test);

You get the same result as

std::vector<std::string> s = foo.do_thing(Bar{test});

-- EDIT --

The OP ask

is there any way of preserving the {test} brace syntax? maybe with initializer_list or something?

Yes... with std::initializer_list

template<typename T>
auto do_thing (std::initializer_list<T> const & l)
{ return do_thing(Bar{*(l.begin())}); }

but, this way, you accept also

std::vector<std::string> s = foo.do_thing(Bar{test1, test2, test3});

using only test1

Maybe a little better... another way can be through a C-style array

template <typename T>
auto do_thing (T const (&arr)[1])
{ return do_thing(arr[0]); }

This way you accept only an element.

Default template parameter cannot be used inside another template parameter?

This:

DefaultType obj; 

uses CTAD (class template argument deduction) which is available since C++17 and only in certain contexts:

  • any declaration that specifies initialization of a variable and variable template
  • new-expressions
  • function-style cast expressions
  • the type of a non-type template parameter:

To instantiate DefaultType with the default argument in other contexts you still need to write DefaultType<>.

I suppose the reason to have CTAD only in certain contexts is that those are the contexts where you always want an instantiation, but never the template.

Why the compiler cannot deduce the template arguments from std::make_shared?

For most variants of std::make_shared which return a std::shared_ptr<T> or std::shared_ptr<U[]> - no parameter has type T. Examples:

template< class T, class... Args >
shared_ptr<T> make_shared( Args&&... args );
template<class T>
shared_ptr<T> make_shared( std::size_t N );

(where in the latter example, T must be U[] for some U).

Without a type-T parameter to those functions - how can you deduce T (or U)?

In your example, you do take a parameter of type T and can easily deduce the template argument.

Why can't the compiler deduce the template parameter when used with a conversion operator?

For class template argument deduction, the "overload set" is composed as described in [over.match.class.deduct/1]. Those are the following:

A set of functions and function templates is formed comprising:

(1.1) - For each constructor of the primary class template designated
by the template-name, if the template is defined, a function template
with the following properties:

(1.1.1) - The template parameters are
the template parameters of the class template followed by the template
parameters (including default template arguments) of the constructor,
if any.

(1.1.2) - The types of the function parameters are those of
the constructor.

(1.1.3) - The return type is the class template
specialization designated by the template-name and template arguments
corresponding to the template parameters obtained from the class
template.

(1.2) - If the primary class template C is not defined or does not
declare any constructors, an additional function template derived as
above from a hypothetical constructor C().

(1.3) - An additional function template derived as above from a
hypothetical constructor C(C), called the copy deduction candidate.

(1.4) - For each deduction-guide, a function or function template with
the following properties:

(1.4.1) - The template parameters, if any,
and function parameters are those of the deduction-guide.

(1.4.2) - The return type is the simple-template-id of the deduction-guide.

As you can see, the matching "function" in 1.1 only attempts to match the argument types to the template parameter types exactly. It doesn't take conversion into account (much like most other template deduction related behavior).

The reason it works for std::pair is due to item 1.3, and the "copy deduction candidate" it defines.



Related Topics



Leave a reply



Submit