Google Groups no longer supports new Usenet posts or subscriptions. Historical content remains viewable.
Dismiss

template operator<<

52 views
Skip to first unread message

Vir Campestris

unread,
Apr 29, 2020, 4:49:40 PM4/29/20
to
I've got a bit of code where I want to serialise collections of Objects.

I have for example

std::vector<Object> vec;
std::list<Object> lst;

It seems to me as if I should be able to write an operator<< to write
them out something like

template <typename Collection>
std::ostream& operator<<(
std::ostream& o,
const Collection& objects) {
for (auto e:objects)
o << e << '\t';
return o;
}

But I can't find a syntax that works. That one above will build - except
then the compiler can't work out which operator<< to use. I'm not
convinced it's even considering that one.

(I'd be happier if the parameter was a Collection<Object> for better
type safety)

Any ideas?

Andy

Pavel

unread,
Apr 29, 2020, 10:47:38 PM4/29/20
to
This seems to work for me:

#include <iostream>
#include <vector>
#include <list>

using namespace std;

struct Object {
};

ostream&
operator<<(ostream &os, const Object &o)
{
return os << "Object " << (void*)&o;
}

vector<Object> vec{Object(), Object()};;
list<Object> lst{Object(), Object(), Object()};

template <typename Collection, typename T = typename Collection::value_type>
ostream&
operator<<(
ostream& o,
const Collection& objects) {
for (const T & e: objects)
o << e << '\t';
return o;
}

int
main(int, char*[])
{
cout << vec << ' ' << lst << endl;
return 0;
}

Alf P. Steinbach

unread,
Apr 30, 2020, 12:40:08 AM4/30/20
to
Detecting that a type is a collection type, without false positives, is
difficult. Consider that e.g. `std::is_same` is an
`std::integral_constant<bool, V>` with V either `true` or `false`. This
is a type with a `value_type`, and implicit conversion to `bool`:


----------------------------------------------------------------
#include <iostream>
#include <type_traits>
using namespace std;

auto main()
-> int
{
cout << boolalpha;
cout << is_same<short, short>() << endl; // "true"
cout << is_same<short, long>() << endl; // "false"
}
----------------------------------------------------------------


But with your `operator<<` template in the picture it will be selected
to output those `std::integral_constant` instances, and treating those
instances as collections will just fail to compile.

So, instead, check for the features that are actually used.

The following appears to work for standard library collections:


template <typename Collection, typename T = typename
Collection::const_iterator>
ostream&
operator<<(
ostream& o,
const Collection& objects) {
for (const auto& e: objects)
o << e << '\t';
return o;
}


However, for a more general solution I think I would define a general
thin wrapper type à la `std::reference_wrapper` to use as argument, and
let the client code explicitly specify (via use of that wrapper type)
that it wants to invoke the for-collections overload of `<<`.

Simply, explicit = good, implicit = not so good, in general.


- Alf

Juha Nieminen

unread,
Apr 30, 2020, 2:24:11 AM4/30/20
to
Alf P. Steinbach <alf.p.stein...@gmail.com> wrote:
> Detecting that a type is a collection type, without false positives, is
> difficult.

Would C++20 concepts help here?

Alf P. Steinbach

unread,
Apr 30, 2020, 7:11:08 AM4/30/20
to
I hope so. But I don't know. Sorry.

- Alf

Vir Campestris

unread,
Apr 30, 2020, 4:26:42 PM4/30/20
to
On 30/04/2020 05:39, Alf P. Steinbach wrote:
> However, for a more general solution I think I would define a general
> thin wrapper type à la `std::reference_wrapper` to use as argument, and
> let the client code explicitly specify (via use of that wrapper type)
> that it wants to invoke the for-collections overload of `<<`.

It's a little hacky project I'm doing purely for my own amusement. It
doesn't need to be clean really :)

But thank you both. It was annoying me!

Andy

Pavel

unread,
May 4, 2020, 1:01:47 AM5/4/20
to
Sorry, I left then without seeing your answer.

I think your answer and mine are equally patchy but both solve the original
problem .. almost (see below). My code (here and below I mean OP's issue only)
explicitly use value_type and both mine and your code implicitly use
const_iterator; each would make Collection deducible by expressing only one of
the container requirements in the additional template parameter and neither
would express all restrictions. To put container subject to bed, if we believe
a collection is a container, we could just use both mine and yours parameters
and by that become more accurate but still inexact as containers have other
requirements. I have never intended to express all restrictions though, just to
solve the issue.

What is more interesting to me, however, is the vague definition of "collection"
in the OP post. Seems both of us have read that as "container". I am now having
a second thought because a better (more general and suitable to OP code)
definition for "collection" would be a range-expression usable in range-based
"for" statement -- because it is its only property that the OP code actually
depends on (other than being able to insert the element to stream; but that the
requirement on element rather than collection type).

Unfortunately, neither mine nor your code works on something like "int a[] =
{5,6,7};"

I actually tried to get it working by changing the second template parameter to
"typename T = decltype(*std::begin(Collection()))"; that works for list and
vector but produces wrong result for int[] (due to automatic conversion to int*,
I believe). I may return to this later unless solved by you or someone else. The
current plan is to define some "range_expression_traits" class template that
would cover all cases of range expressions usable in range-based "for" (again, I
don't see it useful to try to cover all restrictions but rather to make all
interesting usages work although in this case the restrictions are minimal so
they might as well get all covered automatically). I wonder though if some
for-range-expression check or trait already exists so I will be re-inventing the
wheel.

On a side note, I would not want to require client code specify wrapper or
otherwise be anyhow changed; to me the challenge is to make the code that
seemingly should work out-of-box work out-of-box if at all possible.

-Pavel


Richard

unread,
May 5, 2020, 5:57:24 PM5/5/20
to
[Please do not mail me a copy of your followup]

Vir Campestris <vir.cam...@invalid.invalid> spake the secret code
<r8cp8m$hg8$1...@dont-email.me> thusly:

>template <typename Collection>
>std::ostream& operator<<(
> std::ostream& o,
> const Collection& objects) {
> for (auto e:objects)
> o << e << '\t';
> return o;
>}

Even though you've named the template argument "Collection", it might
as well just be T:

template <typename T>
std::ostream &operator<<(std::ostream &o, const T& value)
{
// ...
}

So if I write

std::cout << '\t';

It tries to instantiate your template for T = char and now you have
ambiguous template resolution.

The response from the other poster:

template <typename Collection, T = typename Collection::value_type>
// ...

Makes it such that this is now a template on two arguments and the 2nd
argument relates to the first argument. This can't possibly work for
type 'char', so the compiler no longer places this in the overload set
for 'char'.

In C++20 you would use Concepts to specify that the Collection type
argument adheres to the requirements of iterating over the contents of
the collection with a range for loop. I'm pretty sure that can be
done, but I haven't studied Concepts enough to illustrate the idea
with syntax.
--
"The Direct3D Graphics Pipeline" free book <http://tinyurl.com/d3d-pipeline>
The Terminals Wiki <http://terminals-wiki.org>
The Computer Graphics Museum <http://computergraphicsmuseum.org>
Legalize Adulthood! (my blog) <http://legalizeadulthood.wordpress.com>
0 new messages