Author Topic: Surprisingly Useless C++ Feature  (Read 2228 times)

Offline Hooman

  • Administrator
  • Hero Member
  • *****
  • Posts: 4955
Surprisingly Useless C++ Feature
« on: February 07, 2008, 07:11:39 PM »
How many people knew C++ let's you overload the comma operator?

Code: [Select]
#include <iostream.h>

class A
{
public:
void operator , (int i)
{
  cout << "Overloaded Comma operator" << endl;
};
};
void main()
{
A a;

a, 1;
}

Apparently this was mainly done to preserve orthogonality. ("All the other operators are doing it")  :P

Of course "," has such a low operator prescedence that you pretty much need to use parenthesis to force the compiler to use it. Probably a good thing though, especially when you think of function calls. (Those commas aren't operators, merely punctuation). Anyways, the following code would be treated differently.

Code: [Select]
Func(b,c);   // Normal function call
Func((b,c));   // Invokes the comma operator, and the uses the result for the function parameter

Where the comma is an operator, is in for loops.
Code: [Select]
for (i = 0, j = 100; i < 100; i++, j--)
According to the C++ spec, the comma operator evaluates expressions in a left to right order, and then returns the value of the right-most expression. Which of course means evaluating the expression(s) on the left had better cause a side effect, or it's completely useless. Here's a useless example:
Code: [Select]
int i;
i = 1,2;
cout << i << endl;
i = (1, 2);
cout << i << endl;
The output is:
Code: [Select]
1
2

Why? Because the = operator has a higher prescedence than the comma operator. Keep in mind that i = 1 is an expression in it's own right (and it's value is one). It gets evaluated before the 2 is evaluated. The result of the entire expression is then 2, which is thrown away.

The only place anyone every really uses the comma operator is in for loops, and they always seem to use them for side effects only, and not for returning values. Here's a potential example from the C++ specs on how it could be used to return a value:
Code: [Select]
f(a, (t = 3, t+2), c);
This will pass 5 to the function, but the variable t will be assigned the value 3.


The only real use I was able to find for actually overloading the comma operator, is initializing variables without using the construction name. Example:
Code: [Select]
D d;
d = 1, 2;
To get this to work, however, you have to also overload the = operator. Remember the prescedence rules. It is basically the expression ((d = 1), 2). What you can do is have the = operator return a reference to the object, which the , 2 operator can be applied to.

Code: [Select]
class D
{
public:
D& operator = (int i)
{
  cout << "= operator" << i << endl;
  return *this;
};

D& operator , (int i)
{
  cout << ", operator" << i << endl;
  return *this;
};
}

I suppose you could use this if you wanted to obsfucate some code. Like how macros like to put () around everything, but even then it still looks different. Well, at least overloading it can screw up the order of evaluation. Instead of evaluating from left to right, it becomes function calls, which have whatever order of evaluation the compiler feels like. This is at least evident for globally overloaded operators, which actually have two parameters instead of one. So
Code: [Select]
(exp1, exp2); 
would be like
Code: [Select]
operator,(exp1, exp2);
which might evaluate from right to left.
« Last Edit: February 07, 2008, 07:16:34 PM by Hooman »

Offline nighthalk1

  • Newbie
  • *
  • Posts: 12
Surprisingly Useless C++ Feature
« Reply #1 on: February 07, 2008, 11:32:11 PM »
ya the main use i see for this is to make confusing but short code. like...
v=(i=a+2,b=i/4,c=(b==3),(c-1)*-1)
instead of

i=a+2
b=i/4
if b==3
c=1
v=0
else
c=0
v=1
end if


edit: i just realized, what if one of them comes out to be an illegal assignment, somehow say v=(1,a) but a was never assigned would v come out to be 1? or will the compiler just complain (or if a isnt a number ect ect)
« Last Edit: February 07, 2008, 11:33:53 PM by nighthalk1 »

Offline BlackBox

  • Administrator
  • Hero Member
  • *****
  • Posts: 3093
Surprisingly Useless C++ Feature
« Reply #2 on: February 08, 2008, 11:48:09 AM »
If a was never assigned then there would likely be garbage data. Depending on the compiler it may generate a warning but reading from uninitialized variables is legal in C/C++. (Well, scalar types at least. For the purposes of this argument I will include pointers to objects in the set of "scalar types." Using objects that don't have a default constructor, i.e. ClassName() will cause an error if they are declared without being constructed (and the error will actually be with the line where the variable is declared, since you have to call the constructor at creation time). Likewise, uninitialized references in C++ are not legal).

On the other hand, if a was not declared, the compiler would complain about 'symbol not found' most likely. You have to declare variables before you can use them in C/C++.

By the way, how many compilers will actually let you overload the comma operator? (Personally, I don't know many cases why you would want to.. for example, if you used it in a for loop, then it'd have some kind of weird side effect like that).

Speaking of "unknown" features of C++ (well, C really here), here's one that I'm not sure if too many people are aware of -- the 'restrict' keyword.

It can be applied to pointers, arrays, or references, and it indicates to the compiler that the pointer is not aliased.. in other words, only that pointer is used to access that object.
Here's an example:
Code: [Select]
// Compute the intersection of two integer sets and return the resultant set.
int* intersection(restrict int *p, restrict int *q);
In other words the 'restrict' keyword makes the compiler assume that p and q point to disjoint objects. (Which would generally make sense, since p intersected with p is just equivalent to p, there wouldn't really be any reason to do the intersection).
It can help a bit with optimization. This is defined in the C99 standard, so this isn't some compiler specific feature.
« Last Edit: February 08, 2008, 11:53:27 AM by BlackBox »

Offline Hooman

  • Administrator
  • Hero Member
  • *****
  • Posts: 4955
Surprisingly Useless C++ Feature
« Reply #3 on: February 09, 2008, 03:20:26 AM »
Quote
Which would generally make sense, since p intersected with p is just equivalent to p, there wouldn't really be any reason to do the intersection

Err, say what?


I did read about this a little the other day actually. They had also given an example where if you assumed aliasing was possible, then it's had to reload values from memory after every possible write through another pointer, which would kill efficiency somewhat. It was related to matricies, and I believe the example was something like this:

Code: [Select]
for (j = 0; j < sizey; ++j)
    for (i = 0; i < sizex; ++i)
        m(i, j) += a[i] * a[j];

The problem being, that a[j] would be reloaded each loop iteration because it's memory might overlap with m. Ideally what you'd want, is:

Code: [Select]
for (j = 0; j < sizey; ++j)
{
    float temp = a[j];
    for (i = 0; i < sizex; ++i)
        m(i, j) += a[i] * temp;
}

The idea was, that if you used restrict on the vector being passed in, then the compiler should do this optimization for you, since it knows that "a" won't be modified during the write to "m".


(No, the example was not supposed to be matrix multiply).
(Yes, the example used m(i, j), with an overloaded operator (), as a way of doing multidimensional arrays, instead of using an array of arrays).




Ok, how about template metaprogramming?

If you write something like this:
Code: [Select]
void main()
{
 float x = sin(0.5);
}

Then when the program runs, (provided it isn't optimized to nothing, since there is no output in this simple example), then it will result in a run-time call to the sin function. But we know that since the argument is a compile time constant, the result could also be calculated as a compile time constant. Some programmers do this calculation themselves explicitly to save on the run-time cost, but this makes the code harder to read. A complier could do it, but C++ compilers typically don't do this sort of optimization. (I hear D will, under certain circumstances).

Using template metaprogramming, you can make the calculation of constant values such as this be computed by the compiler at compile time.

Here's some examples using Factorial, and Fibonacci numbers. (Computing them seems like a more obvious example).

Code: [Select]
#include <iostream.h>


// Factorial  (recursive case)
template<int N>
struct Factorial {
    enum { value = N * Factorial<N-1>::value };
};

// Factorial  (base case)
template<>
struct Factorial<1> {
    enum { value = 1 };
};


// Fibonacci  (recursive case)
template<int N>
struct Fib
{
enum { value = Fib<N-1>::value + Fib<N-2>::value };
};

// Fibonacci  (base case 1)
template<>
struct Fib<1>
{
enum { value = 1 };
};

// Fibonacci  (base case 2)
template<>
struct Fib<0>
{
enum { value = 1 };
};


// Quick test
void main()
{
cout << Factorial<2>::value << endl;
cout << Factorial<3>::value << endl;
cout << Factorial<4>::value << endl;
cout << Factorial<5>::value << endl;

cout << endl;

cout << Fib<2>::value << endl;
cout << Fib<3>::value << endl;
cout << Fib<4>::value << endl;
cout << Fib<5>::value << endl;

cout << Fib<43>::value << endl;
cout << Fib<44>::value << endl;
cout << Fib<45>::value << endl;
}


If you turn on assembly listing files, you can see that it actually converts those "calls" to constants. You can see the argument to cout's operator << being pushed onto the stack:

Code: [Select]
	push	2
push 6
push 24
push 120
...
push 2
push 3
push 5
push 8
...
« Last Edit: February 09, 2008, 03:23:53 AM by Hooman »

Offline nighthalk1

  • Newbie
  • *
  • Posts: 12
Surprisingly Useless C++ Feature
« Reply #4 on: February 09, 2008, 10:46:14 AM »
what made me realize the pssible using of variables that arnt assigned yet, is lua lets you do that. though syntax is slightly different, a=1 or b would make a=b if b exists (isnt null), otherwise it would be 1. if for any reason b couldnt be assigned to a, it would go with 1 instead.

Offline BlackBox

  • Administrator
  • Hero Member
  • *****
  • Posts: 3093
Surprisingly Useless C++ Feature
« Reply #5 on: February 09, 2008, 02:26:35 PM »
Quote
Quote
Which would generally make sense, since p intersected with p is just equivalent to p, there wouldn't really be any reason to do the intersection

Err, say what?
Okay, I guess what I meant to say was, if you assume that the pointers are not aliased (so they point to disjoint objects in memory) then you don't really need to do the intersection of A intersected with A (since A is the same object, you can just compare the pointers to see if they are equal).

Of course if you made a copy of set A and called it B then you have two different sets in memory.

Hopefully that made a little bit of sense.

Offline Hooman

  • Administrator
  • Hero Member
  • *****
  • Posts: 4955
Surprisingly Useless C++ Feature
« Reply #6 on: February 10, 2008, 06:29:30 AM »
Oh, heh. I didn't really notice the name of the function in your example source. I feel kind of dumb now.

Also I saw the *p and thought single element, which doesn't make a whole lot of sense.  (AND?). Just thinking it would have been a little more clear if you'd used p[] instead of *p. Just my thoughts anyways, and my experiences with languages that differentiate between pointers to elements and arrays. (Which in my opinion, they should).

Code: [Select]
int* IntersectList(int p[], int q[])

Not that C++ lets you do anything for the return type.



Anyways, just another of those C++ relics that I think needs to be dropped.
I actually like the syntax Java uses for arrays, which could be nice if combined with the pointer syntax for C++. (Don't get me wrong here, I prefer the way C++ does things, I just don't like how it's written sometimes).

So maybe
Code: [Select]
int[] IntersectLists(int[]* list1, int[]* list2)  // Return a new array (by value, hopefully using a hidden parameter to avoid copy)
int[]* IntersectLists(int[]* list1, int[]* list2)  // Return a new array (by ref, probably new-ed)