问题
I have a C++ class "X
" which would have special meaning if a container of them were to be sent to a std::ostream
.
I originally implemented it specifically for std::vector<X>
:
std::ostream& operator << ( std::ostream &os, const std::vector<X> &c )
{
// The specialized logic here expects c to be a "container" in simple
// terms - only that c.begin() and c.end() return input iterators to X
}
If I wanted to support std::ostream << std::deque<X>
or std::ostream << std::set<X>
or any similar container type, the only solution I know of is to copy-paste the entire function and change only the function signature!
Is there a way to generically code operator << ( std::ostream &, const Container & )
?
("Container
" here would be any type that satisfies the commented description above.)
回答1:
If you have read this answer before, you might want to scroll down to the ADL version below. It is much improved.
First, a short and sweet version that pretty much works:
#include <iostream>
#include <type_traits>
template<typename T, typename Iterator, typename=void>
struct is_iterator_of_type: std::false_type {};
template<typename T, typename Iterator>
struct is_iterator_of_type<
T,
Iterator,
typename std::enable_if<
std::is_same<
T,
typename std::iterator_traits< Iterator >::value_type
>::value
>::type
>: std::true_type {};
template<typename Container>
auto operator<<( std::ostream& stream, Container const& c ) ->
typename std::enable_if< is_iterator_of_type<int, typename Container::iterator>::value, std::ostream& >::type
{
return stream << "int container\n";
}
template<typename Container>
auto operator<<( std::ostream& stream, Container const& c ) ->
typename std::enable_if< is_iterator_of_type<double, typename Container::iterator>::value, std::ostream& >::type
{
return stream << "double container\n";
}
which merely detects things that look sort of like int
and double
containers with distinct overloads. I would advise changing the implementation of operator<<
. ;)
A more proper route (thanks @Xeo) would be this adl-hack. We create an auxiliary namespace where we import begin
and end
from std
, then some template functions that do argument dependent lookup on begin
and end
(seeing the std
version if we don't have a more tightly bound one), and then use these aux::adl_begin
functions to determine if what we are passed in can be treated as a container over X:
#include <iostream>
#include <vector>
#include <type_traits>
#include <iterator>
#include <set>
template<typename T, typename Iterator, typename=void>
struct is_iterator_of_type: std::false_type {};
template<typename T, typename Iterator>
struct is_iterator_of_type<
T,
Iterator,
typename std::enable_if<
std::is_same<
T,
typename std::iterator_traits< Iterator >::value_type
>::value
>::type
>: std::true_type {};
namespace aux {
using std::begin;
using std::end;
template<class T>
auto adl_begin(T&& v) -> decltype(begin(std::forward<T>(v))); // no implementation
template<class T>
auto adl_end(T&& v) -> decltype(end(std::forward<T>(v))); // no implementation
}
template<typename T, typename Container, typename=void>
struct is_container_of_type: std::false_type {};
template<typename T, typename Container>
struct is_container_of_type<
T,
Container,
typename std::enable_if<
// we only want this to be used if we iterable over doubles:
is_iterator_of_type<
T,
decltype(void(aux::adl_begin(*(Container*)nullptr)), aux::adl_end(*(Container*)nullptr)) // ensure being and end work as bonus
>::value
>::type
>: std::true_type
{};
template<class Ch, class Tr, class Container>
auto operator<<( std::basic_ostream<Ch,Tr>& stream, Container const& c ) ->
typename std::enable_if<
is_container_of_type<double, Container>::value,
decltype(stream)
>::type
{
stream << "'double' container: [ ";
for(auto&& e:c)
stream << e << " ";
return stream << "]";
}
int main() {
std::cout << std::vector<double>{1,2,3} << "\n";
std::cout << std::set<double>{3.14,2.7,-10} << "\n";
double array[] = {2.5, 3.14, 5.0};
std::cout << array << "\n";
}
With this, not only do arrays of double
s count as containers over double
, so does anything where in its namespace you define a begin
and end
function that returns iterators over double that takes the container as an argument also works. This matches how for(auto&& i:container)
lookup works (perfectly? reasonably well?), so is a good working definition of "container".
Note, however, that as we add more of these embellishments, fewer current compilers have all of the C++11 features we are using. The above compiles in gcc 4.6 I believe, but not gcc 4.5.*.
...
And here is the original short code with some testing framework around it: (useful if your compiler throws it up, you can see where it goes wrong below)
#include <iostream>
#include <type_traits>
#include <vector>
#include <iostream>
#include <set>
template<typename T, typename Iterator, typename=void>
struct is_iterator_of_type: std::false_type {};
template<typename T, typename Iterator>
struct is_iterator_of_type<
T,
Iterator,
typename std::enable_if<
std::is_same<
T,
typename std::iterator_traits< Iterator >::value_type
>::value
>::type
>: std::true_type {};
void test1() {
std::cout << is_iterator_of_type<int, std::vector<int>::iterator>::value << "\n";
}
template<typename T, typename Container>
auto foo(Container const&) -> typename std::enable_if< is_iterator_of_type<T, typename Container::iterator>::value >::type
{
std::cout << "Container of int\n";
}
template<typename T>
void foo(...)
{
std::cout << "No match\n";
}
void test2() {
std::vector<int> test;
foo<int>(test);
foo<int>(test.begin());
foo<int>(std::set<int>());
}
template<typename Container>
auto operator<<( std::ostream& stream, Container const& c ) ->
typename std::enable_if< is_iterator_of_type<int, typename Container::iterator>::value, std::ostream& >::type
{
return stream << "int container\n";
}
void test3() {
std::vector<int> test;
std::cout << test;
std::set<int> bar;
std::cout << bar;
}
template<typename Container>
auto operator<<( std::ostream& stream, Container const& c ) ->
typename std::enable_if< is_iterator_of_type<double, typename Container::iterator>::value, std::ostream& >::type
{
return stream << "double container\n";
}
void test4() {
std::vector<int> test;
std::cout << test;
std::set<int> bar;
std::cout << bar;
std::vector<double> dtest;
std::cout << dtest;
}
void test5() {
std::vector<bool> test;
// does not compile (naturally):
// std::cout << test;
}
template<typename Container>
auto operator<<( std::ostream& stream, Container const& c ) ->
typename std::enable_if< is_iterator_of_type<bool, typename Container::iterator>::value, std::ostream& >::type
{
return stream << "bool container\n";
}
void test6() {
std::vector<bool> test;
// now compiles:
std::cout << test;
}
int main() {
test1();
test2();
test3();
test4();
test5();
test6();
}
about half of the above is testing boilerplate. The is_iterator_of_type
template, and the operator<<
overloads are what you want.
I am presuming that a container of type T
is any class with a typedef iterator
which whose value_type
is a T
. This will cover every std
container, and most custom ones.
Link to execution run: http://ideone.com/lMUF4i -- note that some compilers don't support full C++11 SFINAE, and may require tomfoolery to get it to work.
Test cases left in to help someone check what level of support their compiler has for these techniques.
回答2:
template<template<class T, class A> class container>
std::ostream& opertaor << ( std::ostream&, const container<X, std::allocator<X> > &)
{
}
This won't work if on your implementation vector, list, etc. have more than 2 template parameters.
回答3:
Simple if not elegant - and the next person to maintain your code might appreciate a lack of fancy templates! In practice I would hide the 'Print' method in a cpp, or at least a Detail
namespace.
#include <iostream>
#include <vector>
#include <deque>
#include <list>
#include <set>
#include <multiset>
class X {};
template <typename T>
std::ostream& Print(std::ostream& os, const T& container)
{
for(auto ii = container.cbegin(); ii != container.cend(); ++ii);
//etc
//
return os;
}
std::ostream& operator<<(std::ostream& os, const std::vector<X>& v) { return Print(os, v); }
std::ostream& operator<<(std::ostream& os, const std::deque<X>& v) { return Print(os, v); }
std::ostream& operator<<(std::ostream& os, const std::list<X>& v) { return Print(os, v); }
std::ostream& operator<<(std::ostream& os, const std::set<X>& v) { return Print(os, v); }
std::ostream& operator<<(std::ostream& os, const std::multiset<X>& v) { return Print(os, v); }
int main()
{
// Example
std::vector<X> v;
std::cout << v;
}
回答4:
If you slightly redefine the question to providing special streaming behavior for any class that provides range based access to Widget, instead of special behavior for all Widget containers, one solution is:
template <class Container>
std::ostream& operator << (std::ostream &out, const Container &container)
{
for(const Widget& c : container) {
out << c;
out.put(' ');
}
return out;
}
This works for std::vector
, std::list
, std::deque
, and std::set
. If you attempt to stream something that does not provide range access to Widget, say std::list<int>
, you'll get a compilation error, because the const Widget reference cannot bind to the ints in std::list<int>
. If you provide an overload for operator << for std::list<int>
the code will compile.
回答5:
While @razeh has a nice solution, if you need to get fancy and have specialized printing for a container of X
versus a container of Y
, you can do the following:
// Types for which you want specialized streaming of containers
// We need some identifiable typedef in these types
struct X { typedef void X_type; };
struct Y { typedef void Y_type; };
// Wrappers for implementing streaming logic for each type
template <typename C>
struct WrapX
{
WrapX(const C& c) : c(c) { }
const C& c;
std::ostream& stream(std::ostream& os)
{
// Special container of X printing
return os;
}
};
template <typename C>
struct WrapY
{
WrapY(const C& c) : c(c) { }
const C& c;
std::ostream& stream(std::ostream& os)
{
// Special container of Y printing
return os;
}
};
// Wrap functions, by using a 'dummy' parameter
// we can get the compiler to select the function based
// on the incoming type
template <typename C >
WrapX<C> Wrap(const C& c, typename C::value_type::X_type* = 0) { return WrapX<C>(c); }
template <typename C>
WrapY<C> Wrap(const C& c, typename C::value_type::Y_type* = 0) { return WrapY<C>(c); }
// Overload - same problem as @razeh solution, this is a VERY generic
// function and may clash with other declarations. Keep it closely confined to
// where you need it.
template <typename C>
std::ostream& operator<<(std::ostream& os, const C& c) { return Wrap(c).stream(os); }
int main()
{
std::vector<X> vx;
std::cout << vx;
std::vector<Y> vy;
std::cout << vy;
}
来源:https://stackoverflow.com/questions/13724766/how-to-write-a-streaming-operator-that-can-take-arbitary-containers-of-type