www.digitalmars.com         C & C++   DMDScript  

digitalmars.D.learn - type of if statement used for operator overloading?

reply "Jeremy DeHaan" <dehaan.jeremiah gmail.com> writes:
I've seen operator overloading done 3 different ways. In the 
examples I provide they all compile and work as far as I can 
tell. Is there any major difference between using a static if vs 
a regular if, or any situation that one would be better than the 
other?


struct Something1
{
    Something1 opUnary(string s)()
    {
        if (s == "-")
        {
           return stuff;
        }
    }

}

struct Something2
{
    Something2 opUnary(string s)()
    {
        static if (s == "-")
        {
           return stuff;
        }
    }

}

Also, I realize that if I wanted to overload just a single 
operator and that was all, I could do:

struct Something3
{
    Something3 opUnary(string s)()
       if (s == "-")
    {

        return stuff;

    }

}

But I am more curious about the first two.

Thanks as usual guys!
Jeremy
Feb 05 2013
parent reply Jonathan M Davis <jmdavisProg gmx.com> writes:
On Wednesday, February 06, 2013 07:24:10 Jeremy DeHaan wrote:
 I've seen operator overloading done 3 different ways. In the
 examples I provide they all compile and work as far as I can
 tell. Is there any major difference between using a static if vs
 a regular if, or any situation that one would be better than the
 other?
 
 
 struct Something1
 {
     Something1 opUnary(string s)()
     {
         if (s == "-")
         {
            return stuff;
         }
     }
 
 }
Don't do this. You want to be generating different functions for different overloaded operators. This just introduces extra runtime overhead. It also fails to deal with all of the possible values of s.
 struct Something2
 {
     Something2 opUnary(string s)()
     {
         static if (s == "-")
         {
            return stuff;
         }
     }
 
 }
This is better but again fails to deal with all of the possible values of s. You'll get a nasty error message if a different value gets passed in.
 Also, I realize that if I wanted to overload just a single
 operator and that was all, I could do:
 
 struct Something3
 {
     Something3 opUnary(string s)()
        if (s == "-")
     {
 
         return stuff;
 
     }
 
 }
 
 But I am more curious about the first two.
The last one is by far the best, because it protects against values for s that you don't intend to overload for. You can with the second example if you really want to and have multiple operators that you want to overload with the same function, but you still need a template constraint to protect against values that you don't intend to handle. Pretty much _every_ template that you write should have a template constraint on it. And in many cases, overloaded operators should just use string mixins. For instance, core.time.Duration does this: Duration opBinary(string op, D)(D rhs) safe const pure nothrow if((op == "+" || op == "-") && (is(_Unqual!D == Duration) || is(_Unqual!D == TickDuration))) { static if(is(_Unqual!D == Duration)) return Duration(mixin("_hnsecs " ~ op ~ " rhs._hnsecs")); else if(is(_Unqual!D == TickDuration)) return Duration(mixin("_hnsecs " ~ op ~ " rhs.hnsecs")); } It uses static ifs to differentiate between Duration and TickDuration (since they require different code), but the code doesn't differentiate between the operators. Rather, it just mixes in the string, and it'll do the right thing in both cases. The template constraint already protects against operators that it doesn't actually overload. - Jonathan M Davis
Feb 05 2013
parent reply "Jeremy DeHaan" <dehaan.jeremiah gmail.com> writes:
On Wednesday, 6 February 2013 at 06:38:11 UTC, Jonathan M Davis 
wrote:
 On Wednesday, February 06, 2013 07:24:10 Jeremy DeHaan wrote:
 I've seen operator overloading done 3 different ways. In the
 examples I provide they all compile and work as far as I can
 tell. Is there any major difference between using a static if 
 vs
 a regular if, or any situation that one would be better than 
 the
 other?
 
 
 struct Something1
 {
     Something1 opUnary(string s)()
     {
         if (s == "-")
         {
            return stuff;
         }
     }
 
 }
Don't do this. You want to be generating different functions for different overloaded operators. This just introduces extra runtime overhead. It also fails to deal with all of the possible values of s.
 struct Something2
 {
     Something2 opUnary(string s)()
     {
         static if (s == "-")
         {
            return stuff;
         }
     }
 
 }
This is better but again fails to deal with all of the possible values of s. You'll get a nasty error message if a different value gets passed in.
 Also, I realize that if I wanted to overload just a single
 operator and that was all, I could do:
 
 struct Something3
 {
     Something3 opUnary(string s)()
        if (s == "-")
     {
 
         return stuff;
 
     }
 
 }
 
 But I am more curious about the first two.
The last one is by far the best, because it protects against values for s that you don't intend to overload for. You can with the second example if you really want to and have multiple operators that you want to overload with the same function, but you still need a template constraint to protect against values that you don't intend to handle. Pretty much _every_ template that you write should have a template constraint on it. And in many cases, overloaded operators should just use string mixins. For instance, core.time.Duration does this: Duration opBinary(string op, D)(D rhs) safe const pure nothrow if((op == "+" || op == "-") && (is(_Unqual!D == Duration) || is(_Unqual!D == TickDuration))) { static if(is(_Unqual!D == Duration)) return Duration(mixin("_hnsecs " ~ op ~ " rhs._hnsecs")); else if(is(_Unqual!D == TickDuration)) return Duration(mixin("_hnsecs " ~ op ~ " rhs.hnsecs")); } It uses static ifs to differentiate between Duration and TickDuration (since they require different code), but the code doesn't differentiate between the operators. Rather, it just mixes in the string, and it'll do the right thing in both cases. The template constraint already protects against operators that it doesn't actually overload. - Jonathan M Davis
Thanks for the info, Jonathan! I had no idea that I should constrain the kinds of operators I was overloading if I have more than two, but that makes sense and I will do this from now on. Out of curiosity, what exactly happens when we don't constrain what operators are being overloaded? Is it undefined behavior or will the program just not compile? And that is a cool use of mixins. They are still a pretty new concept to me so I haven't looked into them much. Thanks a lot for the info though! Jeremy
Feb 05 2013
parent Jonathan M Davis <jmdavisProg gmx.com> writes:
On Wednesday, February 06, 2013 08:41:14 Jeremy DeHaan wrote:
 Out of curiosity, what exactly happens when we don't constrain
 what operators are being overloaded? Is it undefined behavior or
 will the program just not compile?
With no template constraint, a template will match anything with the right function arguments, meaning that if you have multiple templates with the same signature, they'll conflict, so you'll get a compilation error. So, for instance, if you had multple opBinary functions, and they didn't have template constraints to distinguish them, then you'd get an error. If you only have one of them and you didn't give a template constraint, then you won't get a conflict, but you'll get nasty error messages which point to the inside of your function when someone tries to use it with bad arguments. It's like what you'd be seeing in C++ (which is known for it's horrible template errors). However, even if you're not overloading a templated function, putting a template constraint on it makes it so that the compiler can give a much more user- friendly message when someone tries to compile it with bad arguments. It also makes it so that the function's body can assume that the condition in the template constraint is true, which can make it cleaner and makes it clearer what arguments it accepts. In addition, you can overload on template constraints, which can be quite powerful (e.g. std.algorithm does this heavily with functions such as find). However, you have to be careful to make sure that only one template matches any given set of arguments, which can make the template constraints much more complicated when you have a lot of overloads.
 And that is a cool use of mixins. They are still a pretty new
 concept to me so I haven't looked into them much.
They're one of the main reasons that D overloads operators the way that it does. - Jonathan M Davis
Feb 05 2013