Reputation: 640
Is there a way in C++ to specify the type of the inner template in a nested template? What I want to do is
template<template <std::string> class StringContainer>
void function(const StringContainer& cont);
In other words, I want a function that accepts all types of string containers -- vector
s, deque
s, list
s, etc.
Basically, I have four questions:
Thanks!
Upvotes: 4
Views: 722
Reputation: 275370
It is possible, but the direction you are going is not a wise one.
Instead, I would advise thinking about things via duck typing. If your function needs to iterate over the contents of the thing passed in, and expects the contents to be strings, what you are looking for is "this argument matches the concept of a sequence of std::string
s".
That is best done in C++14 via the requires syntax, but you probably don't have C++14 compliant compilers yet, unless you are posting via time machine.
We can do this in C++11 via traits classes, SFINAE, and enable_if
. Alternatively, we can do this in C++11 via traits classes and tag dispatching. Tag dispatching is less arcane than SFINAE, so I'll demonstrate it here.
We start with a generic function:
template<typename C>
void function( C const& c )
that accepts anything. Next, we write some traits class:
template<typename C,typename=void>
struct is_string_sequence : std::false_type {};
template<typename C>
struct is_string_sequence<C, typename std::enable_if<TODO STUFF HERE>::type> : std::true_type {};
that tells us if the argument passed in is a sequence over strings.
Next, we write a helper function:
template<typename C>
void function_helper( C const& c, std::true_type string_sequence_test ) {
// your code goes here, and can assume c is a string sequence
}
and forward the first one to it:
template<typename C>
void function( C const& c ) {
function_helper( c, is_string_sequence<C>() );
}
but we also create a tag to pass to the helper. This tag is true if our tests pass, and false if not. As we only have an override for true, the false case generates an error, and that error tends to be reasonably readable.
The only thing left is to write TODO STUFF HERE
.
The duck-type way to tell if something is a string sequence in C++11 is to see if:
for( std::string s : c ) {
}
compiles. A way to do this is to see if begin
and end
free functions applied to c
in a context where std::begin
and std::end
are visible returns iterators, and those iterators when dereferenced produce a type that is convertible to std::string
.
This requires some gymnastics. First, seeing what begin
and end
result in when called in that particular context:
namespace adl_helper {
using std::begin; using std::end;
template<typename C>
auto adl_begin( C&& c )->decltype( begin(std::forward<C>(c)) );
template<typename C>
auto adl_end( C&& c )->decltype( end(std::forward<C>(c)) );
}
using adl_helper::adl_begin; using adl_helper::adl_end;
note that these have no bodies: for our purposes, we don't need bodies, we just need the return types.
Now we can ask "is something a container?"
template<typename C, typename=void>
struct is_container : std::false_type {};
template<typename C>
struct is_container<C, typename=typename std::enable_if<
std::is_convertible<
decltype( adl_begin( std::declval<C&>() ) == adl_end( std::declval<C&>() ) ),
bool
>::value
>::type> : std::true_type
{
typedef decltype( adl_begin( std::declval<C&>() ) ) iterator;
};
a fancier one might probe into iterator_traits
instead (above, I just check that the begin
and end
work, and the results have an overridden ==
between them that returns something that can be converted to bool
).
We can then ask if the resulting iterator has a value type that can be converted to std::string
in another traits class.
Alternatively we can do a decltype( *adl_begin( std::declval<C&>() ) )
, and see if that can be directly converted to std::string
, and assume if begin
works so will end
.
The SFINAE method is similar, except instead of doing the tag dispatching to a helper, we put the test directly into the function
signature, wrapped in an enable_if
that activates or deactivates the function
for overload resolution. This generates slightly harder to read errors but at a higher point in the call stack in my experience.
You don't do the approach you where aiming at, because the details of how the container was built from other types are not important: what is important is how you use it.
If you need other features form your type other than iteration, you can write other duck-type tests that determine if you can erase, insert, etc. However, note that associative containers and sequential containers are very different things, and treating them uniformly is rarely a good idea, other than in that both are sequences.
Upvotes: 1
Reputation:
#include <deque>
#include <list>
#include <iostream>
#include <vector>
// Removed:
// template < template<typename, typename ...> class Container,
// typename T, typename ...Types>
// void function(const Container<T, Types...>&);
template < template<typename ...> class Container, typename ...Types>
void function(const Container<std::string, Types...>& container) {
for(const std::string& s: container) {
std::cout << s << " ";
}
std::cout << std::endl;
}
int main () {
std::deque<std::string> d{ "Hello", "Deque" };
function(d);
std::list<std::string> l{ "Hello", "List" };
function(l);
std::vector<std::string> v{ "Hello", "Vector" };
function(v);
return 0;
}
Upvotes: 3