2

I was thinking a lot what title to put on my question and failed anyway, so if you find a good one, please edit it.

I am trying to write a print function for a vector or other container<T> and have another print function for container<container<T>>, so here what I came up with:

template<typename T>
void print(T const& cont){
    for (const auto& i : cont) {
        cout << i << " ";
    }
    cout << endl;
}

template<typename T, template<typename> typename Cont>
void print(Cont<T> const& cont) {
    for (const auto& i : cont) {
        print(i);
    }
}

and I have my 2 target containers here:

vector<vector<int>> subsets;
vector<int> subset;

When I invoke print(subset); the program works as expected, but when i invoke print(subsets), compiler starts complaining:

error C2679: binary '<<': no operator found which takes a right-hand operand of type 'const std::vector<int,std::allocator<int>>' (or there is no acceptable conversion)

My conclusion is that its still trying to call the non-nested template print function and failing on cout as I am trying to cout a vector.

Can anyone explain why the overload resolution is not working as I expect and what I did wrong here? Even when I rename the nested-template function to printn, its started to complain for a different reason:

error C2784: 'void prints(const Cont<T> &)': could not deduce template argument for 'const Cont<T> &' from 'std::vector<std::vector<int,std::allocator<int>>,std::allocator<std::vector<int,std::allocator<int>>>>'
NathanOliver
  • 171,901
  • 28
  • 288
  • 402
Eduard Rostomyan
  • 7,050
  • 2
  • 37
  • 76

3 Answers3

5

The short, simple and insufficient answer is that std::vector has 2 template parameters. You should also include some spacing:

template<class T, class A, template<class, class>class C>
void print(C<T,A> const& cont) {
  std::cout << "[ ";
  bool bFirst = true;
  for (const auto& i : cont) {
    if (!bFirst)
      std::cout << ", ";
    bFirst = false;
    print(i);
  }
  std::cout << " ]";
}

So that overload was never called.

Once you do this, your code doesn't work, because you don't have an element-printer. So replace your other loop-printer with an element-printer:

template<typename T>
void print(T const& i){
  std::cout << i;
}

Live example.

Test code:

std::vector<int> a={1,2,3};
print(a);
std::cout << "\n";
std::vector<std::vector<int>> b = {a, a, a};
print(b);
std::cout << "\n";

Output:

[ 1, 2, 3 ]
[ [ 1, 2, 3 ], [ 1, 2, 3 ], [ 1, 2, 3 ] ]

this is insufficient, because you really should do something fancier to detect "is this object iterable" and "is this object tuple-like" if you want a more serious general purpose printer. Detecting Cont<A,B> templates is a poor substitute.

Here is code to detect if something is iterable (ignore the bad checked answer, read the one I linked to).

Then do a SFINAE test for "is the argument iterable" in print for the one that does a for(:) loop.

The next thing you'll want to do is detect if the object is tuple-like. If it is, you want to print out each element of the tuple. This give you std::map and std::unordered_map support. Note that a std::array is both tuple-like and iterable.

This is a bit harder than detecting "iterable", and varies more with which C++ standard you are working with, because tuple-like-ness is expanding with new versions of C++. You can be lazy and just detect std::pair and std::tuple, which will cover 99% of use cases.

Yakk - Adam Nevraumont
  • 262,606
  • 27
  • 330
  • 524
3

Your issue here is that std::vector has more than one template type. Because of this, the T overload is use instead. The reason this happens is from an ambiguity in the language on how default template parameters should be considered in template template parameter. This led to the defect report DR150 which was adopted in C++17 and will allow your code to work in a compliant compiler1. To work around this you can use a variadic template template parameter and adjust the base case to just print the element like

template<typename T>
void print(T const& elem){
    cout << elem << " ";
}

template<template<typename...> typename Cont, typename... Params>
void print(Cont<Params...> const& cont) {
    for (const auto& i : cont) {
        print(i);
    }
    cout << endl;
}

int main()
{
    vector<vector<int>> subsets{{1,2},{3,4}};
    print(subsets);
}

outputs

1 2 
3 4 

1: I had to adjust the base case like in my example because now the template temaplte version will call the nested vector

NathanOliver
  • 171,901
  • 28
  • 288
  • 402
2

You can also use SFINAE (or C++ Concepts, if you live in the future) to get the result you want without having to know how many template params your incoming container has. Here's an example using trailing return types to do SFINAE:

#include <iostream>
#include <vector>

template<typename T>
auto print( const T& cont ) -> decltype(std::cout << *begin(cont), void()) 
{
    for( auto&& i : cont )
        std::cout << i << ' ';
    std::cout << '\n';
}

template<typename T>
auto print( const T& cont ) -> decltype(begin(*begin(cont)), void())
{
    for( auto&& i : cont )
        print(i);
}

int main()
{
    const auto subset1  = std::vector<int>{ 1, 2, 3, 4 };
    const auto subset2 = std::vector<std::vector<int>>{ {5,6,7}, {8,9} };
    const auto subset3 = std::vector<std::vector<std::vector<int>>>{ 
        { {10,20,30}, {40,50} }, 
        { {60}, {70,80,90}, {100,110,120} },
        { {200,400,600} }
    };

    print( subset1 );
    print( subset2 );
    print( subset3 );
}

See it live on Coliru where it outputs:

1 2 3 4 
5 6 7 
8 9 
10 20 30 
40 50 
60 
70 80 90 
100 110 120 
200 400 600 

Note how the first function expresses that it requires the ability to write a single element via the begin() function (implicitly used by range-for loops) and the second requires at least doubly nested sequences accessible via begin().

I'd probably lean toward a variadic template myself unless you need to handle containers separately from containers of containers (of containers (of containers...)).

metal
  • 6,202
  • 1
  • 34
  • 49
  • `print( std::vector>>{} )` doesn't work using this technique. A naive solution is to replace the 2nd `cout << cont[0][0]` with `print(cont[0])`, but then you need to enable ADL (say, with a trampoline and a tag) or it won't be spotted on the recursive lookup. Also, `for(:)` and `[0]` are different operations, you are testing for something that you aren't using, and using something you aren't testing for. – Yakk - Adam Nevraumont May 29 '20 at 14:45
  • @Yakk-AdamNevraumont Fair points. I have updated my answer to rely on `begin()` instead. Does that resolve your complaints? – metal May 29 '20 at 14:54
  • thanks for the answer @metal, is there an article or link for me to understand how this decltype works? I am not really following the syntax. – Eduard Rostomyan May 29 '20 at 16:06
  • It's a simplified version of the old `std::enable_if` idiom. The first thing to note is that `decltype` returns the type of its parameter without actually running the code therein -- it just says, "if I were to run this code, what type would result?" Then it sneakily uses the comma operator to discard the first part and just make the return type always `void`. – metal May 29 '20 at 16:13
  • It might become clearer if you consider a statement like this: `static_assert( std::is_same< decltype(float{}, int{}), int>::value )`. It asserts at compile-time that the `decltype` expression is of type `int`. `decltype` is evaluating the expression ", ", and the comma operator discards the left-most item and keeps just the last. Does that clarify? – metal May 29 '20 at 16:16