When to Overload the Comma Operator

When to Overload the Comma Operator?

Let's change the emphasis a bit to:

When should you overload the comma?

The answer: Never.

The exception: If you're doing template metaprogramming, operator, has a special place at the very bottom of the operator precedence list, which can come in handy for constructing SFINAE-guards, etc.

The only two practical uses I've seen of overloading operator, are both in Boost:

  • Boost.Assign
  • Boost.Phoenix – it's fundamental here in that it allows Phoenix lambdas to support multiple statements

Operator comma overloading

There is already a built-in definition for the comma operator applied to ints. Your template isn't even in the running for overload resolution, since you can't overload operators unless at least one of the arguments is a user defined type.

You could do something like this:

template<typename T>
struct vector_maker
{
std::vector<T> vec;
vector_maker& operator,(T const& rhs) {
vec.push_back(rhs);
return *this;
}

std::vector<T> finalize() {
return std::move(vec);
}
};

int main() {
auto a = (vector_maker<int>(),1,2,3,4,5).finalize();
}

Or take a look at Boost.Assign, which allows constructions like this:

std::vector<int> a;
a += 1,2,3,4,5,6,7,8;

The void(), the comma operator (operator,) and the impossible (?) overloading

The relevant clause for this is 13.3.1.2/9 [over.match.oper] in N4140:

If the operator is the operator ,, the unary operator &, or the operator ->, and there are no viable functions,
then the operator is assumed to be the built-in operator and interpreted according to Clause 5.

As void() is never a valid function argument (see 5.2.2/7 [expr.call]), there never is a viable function and thus the built-in , will be used.

So no, what you are trying to do is not possible.

In fact, writing an iterator loop like this

for(...; ++it1, (void)++it2)

is a standard way to prevent users from breaking your code by overloading , for their iterator types by enforcing the built-in operator , to be used. (Note that I am not saying you need to do this in your everyday code. It very much depends on its actual use. This is standard library level of paranoid.)


Regarding the standard clause you linked:

The meaning of the operators =, (unary) &, and , (comma), predefined for each type, can be changed for specific class and enumeration types by defining operator functions that implement these operators.

But such a function cannot be defined because, as I said above, void() is never a valid function argument.

Now whether or not this is an oversight/problem in the standard is open to debate.

How to overload operator to take a comma separated variable argument list

You'll have to overload the insertion operator (<<) and the comma operator (,) such that

mat << 1.0, 2.0;

is translated as:

mat.operator<<(1.0).operator,(2.0);

or

operator,(operator<<(mat, 1.0), 2.0);

Here's a demonstrative program that illustrates the idea without doing anything useful.

struct Foo
{
};

Foo& operator<<(Foo& f, double)
{
std::cout << "In operator<<(Foo& f, double)\n";
return f;
}

Foo& operator,(Foo& f, double)
{
std::cout << "In operator,(Foo& f, double)\n";
return f;
}

int main()
{
Foo f;
f << 10, 20, 30;
}

and its output

In operator<<(Foo& f, double)
In operator,(Foo& f, double)
In operator,(Foo& f, double)

How to overload a comma operator to assign values to an array

How do I implement the comma operator so that obj = 1, 2, 7 will do the
exact same thing as doing obj.arr[0] = 1; obj.arr[1] = 2; obj.arr[2] = 7;?

This would completely change the meaning of comma operator. I will prefer initializer list:

obj = {1, 2, 7};

over the usage of comma operator in this scenario.

I know from research that overloading comma operator is unusual, yet I
need to do it as it's in the requirements of the project.

Yes, I have met such teachers. I think they just want to test whether you can crack their task under these strange constraints. And my solution is based on the hidden clue in your question itself.

What I know so far is that obj = 1, 2, 7 is equivalent to
obj.operator=(1).operator,(2).operator,(7);

Exactly. Notice how operator, is almost synonymous to operator= in this task:

obj.operator=(1).operator=(2).operator=(7);

So, it's just a matter of implementing this trick:

Sample& Sample::operator,(const int& val)
{
// simply reuse the assignment operator
*this = val;

// associativity of comma operator will take care of the rest
return *this;
}

Implementing operator= is upto you.

Then you can do

obj = 1, 2, 7;

I've made a small working code similar to your example: Live Demo.

Edit:

Following Jarod's comment, which proposes a more reasonable overloading of these operators, you can overload operator= in this way (clear + push_back):

Sample& Sample::operator=(const int& val)
{
arr[0] = val;
length = 1;
return *this;
}

And operator, in this way (push_back):

Sample& Sample::operator,(const int& val)
{
// append the value to arr
arr[length] = val;
++length;

// associativity of comma operator will take care of the rest
return *this;
}

Putting this idea together: Demo 2

Why doesn't my overloaded comma operator get called?

void operator,(const float &rhs)

You need a const here.

void operator,(const float &rhs) const {
cout << this->val << ", " << rhs << endl;
}

The reason is because

rhs, lhs

will call

rhs.operator,(lhs)

Since rhs is a const comma_op&, the method must be a const method. But you only provide a non-const operator,, so the default definition will be used.

C++ comma operator overloading and vector of references

I'm using C++11 to outline you a solution (the code is tested, too), but this is doable in C++03 (using e.g. Boost.Tuple):

// Base case
template<typename Lhs, typename Rhs>
std::tuple<Lhs&, Rhs&>
operator,(Lhs& lhs, Rhs& rhs)
{
return std::tie(lhs, rhs);
}

// General case when we already have a tuple
template<typename... Lhs, typename Rhs>
std::tuple<Lhs..., Rhs&>
operator,(std::tuple<Lhs...>&& lhs, Rhs& rhs)
{
return std::tuple_cat(lhs, std::tie(rhs));
}

Usage looks like (assuming this operator resides in namespace ns):

// Declaration: note how ff must return a tuple
std::tuple<X, Y, Z> ff(A, B, C);

A a = /* ... */;
B b = /* ... */;
C c = /* ... */;
X x; Y y; Z z;

using ns::operator,;
// brackets on the left-hand side are required
(x, y, z) = ff(a, b, c);

Here are the attached caveats:

  • as you've seen, you need a using declaration to bring operator, in scope. Even if the types X, Y, Z reside in the same scope of operator, (to enable ADL), std::tuple doesn't. You could do something like template<typename... T> struct tuple: std::tuple<T...> { using std::tuple<T...>::tuple; }; (not as convenient to do in C++03 however) to have your own tuple in the appropriate namespace to have ADL in a quick-and-dirty way. However:

  • overloaded operators must always operate on at least one user-defined type. So if types X and Y both happen to be types like int or double, then you get the default operator,. The usual solution to things like this is to require the client to do instead something like (ref(x), ref(y), z) = ff(a, b, c); where ref will return a type in the appropriate namespace (for ADL purposes, again). Perhaps such type can be implemented in terms of std::reference_wrapper (or the Boost version, for C++03) with the same quick-and-dirty hack as for the tuple. (You'd need additional overloads of operator,.)

All in all, that's a lot of work (with ugly workarounds) when something like

/* declaration of ff and variable definitions the same as before */
std::tie(x, y, z) = ff(a, b, c);

or perhaps

/* this skips unnecessary default constructions of x, y, z */
auto tuple = ff(a, b, c);
using std::get;
auto& x = get<0>(tuple);
auto& y = get<1>(tuple);
auto& z = get<2>(tuple);

works out of the box (even in C++03 with Boost.Tuple). My advice in this matter would be (with no slight intended): Keep it simple, stupid! and to use that.

C++ overloading operator comma for variadic arguments

It is sort-of possible, but the usage won't look very nice. For exxample:

#include <vector>
#include <iostream>
#include <algorithm>
#include <iterator>

template <class T>
class list_of
{
std::vector<T> data;
public:
typedef typename std::vector<T>::const_iterator const_iterator;
const_iterator begin() const { return data.begin(); }
const_iterator end() const { return data.end(); }

list_of& operator, (const T& t) {
data.push_back(t);
return *this;
}
};

void print(const list_of<int>& args)
{
std::copy(args.begin(), args.end(), std::ostream_iterator<int>(std::cout, " "));
}

int main()
{
print( (list_of<int>(), 1, 2, 3, 4, 5) );
}

This shortcoming will be fixed in C++0x where you can do:

void print(const std::initializer_list<int>& args)
{
std::copy(args.begin(), args.end(), std::ostream_iterator<int>(std::cout, " "));
}

int main()
{
print( {1, 2, 3, 4, 5} );
}

or even with mixed types:

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

template <class Arg1, class ...ArgN>
void print(const Arg1& a1, const ArgN& ...an)
{
std::cout << a1 << ' ';
print(an...);
}


int main()
{
print( 1, 2.4, 'u', "hello world" );
}


Related Topics



Leave a reply



Submit