C++ template template argument types deduction

Viewed 738

I have code that finds and prints out matches of a pattern as going over the container of strings. Printing is performed in the function foo that is templated

The code

#include <iostream>
#include <algorithm>
#include <iterator>
#include <vector>
#include <string>
#include <tuple>
#include <utility>

template<typename Iterator, template<typename> class Container>
void foo(Iterator first, Container<std::pair<Iterator, Iterator>> const &findings)
{
    for (auto const &finding : findings)
    {
        std::cout << "pos = " << std::distance(first, finding.first) << " ";
        std::copy(finding.first, finding.second, std::ostream_iterator<char>(std::cout));
        std::cout << '\n';
    }
}

int main()
{
    std::vector<std::string> strs = { "hello, world", "world my world", "world, it is me" };
    std::string const pattern = "world";
    for (auto const &str : strs)
    {
        std::vector<std::pair<std::string::const_iterator, std::string::const_iterator>> findings;
        for (std::string::const_iterator match_start = str.cbegin(), match_end;
             match_start != str.cend();
             match_start = match_end)
        {
            match_start = std::search(match_start, str.cend(), pattern.cbegin(), pattern.cend());
            if (match_start != match_end)
                findings.push_back({match_start, match_start + pattern.size()});
        }
        foo(str.cbegin(), findings);
    }

    return 0;
}

When compiling I've got an error that types deduction has failed due to inconsistency of iterators being provided, their types turn out to be diverse.

GCC compilation error:

prog.cpp:35:9: error: no matching function for call to 'foo'
        foo(str.cbegin(), findings);
        ^~~
prog.cpp:10:6: note: candidate template ignored: substitution failure [with Iterator = __gnu_cxx::__normal_iterator<const char *, std::__cxx11::basic_string<char> >]: template template argument has different template parameters than its corresponding template template parameter
void foo(Iterator first, Container<std::pair<Iterator, Iterator>> const &findings)
     ^
1 error generated.

Clang's output:

main.cpp:34:9: error: no matching function for call to 'foo'
        foo(str.cbegin(), findings);
        ^~~
main.cpp:9:6: note: candidate template ignored: substitution failure [with Iterator = std::__1::__wrap_iter<const char *>]: template template argument has different template parameters than its corresponding template template parameter
void foo(Iterator first, Container<std::pair<Iterator, Iterator>> const &findings)

What am I not catching? Is my utilization of template template types deduction wrong and appears an abuse from the standard's point of view? Neither g++-9.2 with listdc++11 nor clang++ with libc++ are able to compile this.

2 Answers

Your code should work fine since C++17. (It compiles with gcc10.)

The template template argument std::vector has two template parameters (the 2nd one has default argument std::allocator<T>), but the template template parameter Container has only one. Since C++17 (CWG 150), the default template arguments are allowed for template template argument to match template template parameter with fewer template parameters.

template<class T> class A { /* ... */ };
template<class T, class U = T> class B { /* ... */ };

template<template<class> class P> class X { /* ... */ };

X<A> xa; // OK
X<B> xb; // OK in C++17 after CWG 150
         // Error earlier: not an exact match

Before C++17, you can define the 2nd template parameter with default argument for the template template parameter Container, e.g.

template<typename Iterator, template<typename T, typename Alloc=std::allocator<T>> class Container>
void foo(Iterator first, Container<std::pair<Iterator, Iterator>> const &findings)

Or apply parameter pack.

template<typename Iterator, template<typename...> class Container>
void foo(Iterator first, Container<std::pair<Iterator, Iterator>> const &findings)

In some versions of C++, Container can't match std::vector, because std::vector isn't actually a template <typename> class. It's a template <typename, typename> class where the second parameter (the allocator type) has a default template argument.

Although it could work to add another template parameter typename Alloc make the function parameter Container<std::pair<Iterator, Iterator>, Alloc>, that could be an issue for other container types.

But since your function doesn't actually use the template template parameter Container, there's no need to require such a complicated template argument deduction, with all the gotchas and limitations of deducing a template template argument:

template<typename Iterator, class Container>
void foo(Iterator first, Container const &findings);

This also doesn't require Iterator to be deduced as the exact same type in three different places. Meaning it will be valid to pass a X::iterator as first and a container containing X::const_iterator or vice versa, and template argument deduction could still succeed.

The one slight drawback is that if another template uses SFINAE techniques to try to determine whether a signature of foo is valid, that declaration would match nearly anything, like foo(1.0, 2). This often isn't important for a specific-purpose function, but it's nice to be more restrictive (or "SFINAE-friendly") at least for general purpose functions. We could add a basic restriction with something like:

// Require Container is container-like (including raw array or std::initializer_list)
// and its values have members first and second of the same type,
// which can be compared for equality with Iterator.
template <typename Iterator, class Container>
auto foo(Iterator first, Container const &findings)
    -> std::void_t<decltype(first == std::begin(findings)->first),
           std::enable_if_t<std::is_same_v<std::begin(findings)->first, 
                            std::begin(findings)->second>>>;
Related