www.digitalmars.com         C & C++   DMDScript  

digitalmars.D.learn - Operator Overloading and boilerplate code

reply Loopback <elliott.darfink gmail.com> writes:
Hi!

I've made a two dimensional vector structure (x, y) which is very
primitive, the only real thing that I want to do with this structure
is support for nearly all operators and support for D3DXVECTOR2 and
POINT structures.

While writing this structure the boilerplate code has steadily
increased. Since I am new to D I mainly wanted to ask if there are any
solutions to prevent this amount of boilerplate code with mixins etc...

The unittest, also, fail to compile. When I am trying to use the
in-place assignment operator the compiler complains saying this;

Error: function dx.directx.DVECTOR2.opAssign (ref const const(DVECTOR2) 
rhs) is not callable using argument types (DVECTOR2)
Error: cannot implicitly convert expression (this.opBinary(rhs)) of type 
DVECTOR2 to float
Error: template instance dx.directx.DVECTOR2.opOpAssign!("+") error 
instantiating

The first two errors origins from the last opOpAssign operator (taking
a float argument) whilst the last error is from the "test += 2f"
statement.

struct DVECTOR2
{
	float x = 0f;
	float y = 0f;
	
	this(float x, float y)
	{
		this.x = x;
		this.y = y;
	}
	
	this(float xy)
	{
		this.x = xy;
		this.y = xy;
	}
	
	// Implement D3DXVECTOR2 support
	this(const ref D3DXVECTOR2 vec)
	{
		this.x = vec.x;
		this.y = vec.y;
	}
	
	// Implement POINT support
	this(const ref POINT pnt)
	{
		this.x = pnt.x;
		this.y = pnt.y;
	}
	
	// Return a inverse vector
	DVECTOR2 opUnary(string op)() if(op == "-") { return DVECTOR2(-x, -y); }
	
	// The Binary Operators
	DVECTOR2 opBinary(string op)(DVECTOR2 rhs) { return DVECTOR2(mixin("x" 
~ op ~ "rhs.x"), mixin("y" ~ op ~ "rhs.y")); }
	DVECTOR2 opBinary(string op)(D3DXVECTOR2 rhs) { return 
opBinary!op(DVECTOR2(rhs)); }
	DVECTOR2 opBinary(string op)(POINT rhs) { return 
opBinary!op(DVECTOR2(rhs)); }
	DVECTOR2 opBinary(string op)(float rhs) { return 
opBinary!op(DVECTOR2(rhs)); }
	
	// Right Operators Support
	DVECTOR2 opBinaryRight(string op)(DVECTOR2 lhs) { return 
lhs.opBinary!op(this); }
	DVECTOR2 opBinaryRight(string op)(D3DXVECTOR2 lhs) { return 
DVECTOR2(lhs).opBinary!op(this); }
	DVECTOR2 opBinaryRight(string op)(POINT lhs) { return 
DVECTOR2(lhs).opBinary!op(this); }
	DVECTOR2 opBinaryRight(string op)(float lhs) { return 
DVECTOR2(lhs).opBinary!op(this); }
	
	// Assignment Operator
	ref DVECTOR2 opAssign(const ref DVECTOR2 rhs) { x = rhs.x; y = rhs.y; 
return this; }
	ref DVECTOR2 opAssign(const ref D3DXVECTOR2 rhs) { return 
(this.opAssign(DVECTOR2(rhs))); }
	ref DVECTOR2 opAssign(const ref POINT rhs) { return 
(this.opAssign(DVECTOR2(rhs))); }
	ref DVECTOR2 opAssign(float rhs) { return (this.opAssign(DVECTOR2(rhs))); }

	// In-Place Assignment Operators
	ref DVECTOR2 opOpAssign(string op)(DVECTOR2 rhs) { return 
(this.opAssign(opBinary!op(rhs))); }
	ref DVECTOR2 opOpAssign(string op)(D3DXVECTOR2 rhs) { return 
(this.opAssign(opBinary!op(rhs))); }
	ref DVECTOR2 opOpAssign(string op)(POINT rhs) { return 
(this.opAssign(opBinary!op(rhs))); }

	// This is where the two first errors occur
	ref DVECTOR2 opOpAssign(string op)(float rhs) { return 
(this.opAssign(opBinary!op(rhs))); }
	
	// Operator Casts
	D3DXVECTOR2 opCast(T)() if(is(T == D3DXVECTOR2)) { return 
D3DXVECTOR2(x, y); }
	POINT opCast(T)() if(is(T == POINT)) { return POINT(cast(int) x, 
cast(int) y); }
}

unittest
{
	DVECTOR2 test = DVECTOR2(2f, 2f);
	test += 2f; // This is where the last error occurs
	
	assert(test.x == 4f);
}
Jul 02 2011
parent reply Timon Gehr <timon.gehr gmx.ch> writes:
Your example with reduced boilerplate code:



struct DVECTOR2 {
 template Accepts(T){enum Accepts=is(T==DVECTOR2) || is(T==float) ||
is(T==D3DXVECTOR2) || is(T==POINT);}
 template isScalar(T){enum isScalar=is(T==float);}
 float x = 0f, y=0f;

 this()(float x, float y) {
  this.x = x;
  this.y = y;
 }

 this()(float xy) { this(xy,xy); }

 // Implement D3DXVECTOR2 and POINT support
 this(T)(T arg) if(Accepts!T && !isScalar!T) {
  this.x = arg.x;
  this.y = arg.y;
 }

 DVECTOR2 opUnary(string op)() if(op == "-") { return DVECTOR2(-x, -y); }
 DVECTOR2 opBinary(string op,T)(T rhs) if(Accepts!T) {
  enum rx="rhs"~isScalar!T?"":".x", ry=isScalar!T?"":".y";
  return DVECTOR2(mixin("x" ~ op ~ "rhs"~rx), mixin("y" ~ op ~ "rhs"~ry));
 }
 DVECTOR2 opBinaryRight(string op,T)(T lhs) if(Accepts!T) { return
DVECTOR2(lhs).opBinary!op(this); }
 ref DVECTOR2 opAssign(T)(T rhs) if(Accepts!T) {
  static if(isScalar!T) x=y=rhs; else x=rhs.y, y=rhs.y; return this;
 }
 ref DVECTOR2 opOpAssign(string op,T)(T rhs) if(Accepts!T) {
return(this.opAssign(opBinary!op(rhs))); }
 T opCast(T)() if(Accepts!T && !isScalar!T) { return T(x, y); }
}

unittest
{
 DVECTOR2 test = DVECTOR2(2f, 2f);
 test += 2f;
 assert(test.x == 4f);
}

Should work.

Cheers,
-Timon
Jul 02 2011
next sibling parent Loopback <elliott.darfink gmail.com> writes:
Thank you!

How excellent you handled the boilerplate code. Works perfectly fine as
well, except one little thing. I cannot declare this DVECTOR2 structure
immutable because the compiler complains saying this:

Error: variable __ctmp1485 cannot be read at compile time
Error: cannot evaluate __ctmp1485.this(0F,1F,0F) at compile time

As I mentioned before, I am new to D and I do not understand why the
compiler cannot evaluate the expression at compile time.

One more thing which I also am wondering is how opCast really works.
To trigger opCast you need to do an explicit cast (tell me if I'm wrong)
and when you do this, does it also work for cases like this:

cast(D3DXVECTOR2) &vector2

Does this code trigger the defined opCast (vector2 is a DVECTOR2
structure in this case) or do I have to define a new opCast in
cases where the address operator is used or is the compiler
able to distinguish the cases and cast opCast before the address
operator is evaluated?

struct DVECTOR2
{
	// Controls that the parameter is a valid type
	template Accepts(T) { enum Accepts = is(T == DVECTOR2) || is(T == 
float) || is(T == D3DXVECTOR2) || is(T == POINT); }
	
	// Whether the parameter is a float or not
  	template isScalar(T) { enum isScalar = is(T == float); }
	
	// The Variables
	float x = 0f;
	float y = 0f;

	// Default Constructor
	this()(float x, float y)
	{
		this.x = x;
		this.y = y;
	}

	// Float Constructor
  	this()(float xy) { this(xy, xy); }

	// Implement D3DXVECTOR2 and POINT support
	this(T)(T arg) if(Accepts!T && !isScalar!T) { this(arg.tupleof); }

	// Inverse the vector
	DVECTOR2 opUnary(string op)() if(op == "-") { return DVECTOR2(-x, -y); }
	
	// Binary Operations
	DVECTOR2 opBinary(string op, T)(T rhs) if(Accepts!T)
	{
		enum rx = isScalar!T ? "" : ".x";
		enum ry = isScalar!T ? "" : ".y";
		
		return DVECTOR2(mixin("x" ~ op ~ "rhs" ~ rx), mixin("y" ~ op ~ "rhs" ~ 
ry));
	}
	
	// Right Binary Operator
	DVECTOR2 opBinaryRight(string op, T)(T lhs) if(Accepts!T) { return 
DVECTOR2(lhs).opBinary!op(this); }
	
	// Assign Operator
	ref DVECTOR2 opAssign(T)(T rhs) if(Accepts!T)
	{
		static if(isScalar!T)
			x = y = rhs;
			
		else
		{
			x = rhs.x;
			y = rhs.y;
		}
		
		return this;
	}
	
	// In-Place Assignment Operators
	ref DVECTOR2 opOpAssign(string op, T)(T rhs) if(Accepts!T) { 
return(this.opAssign(opBinary!op(rhs))); }

	// Cast Operators (to D3DXVECTOR2 and POINT)
	T opCast(T)() if(Accepts!T && !isScalar!T) { return T(x, y); }
}
Jul 03 2011
prev sibling parent reply Loopback <elliott.darfink gmail.com> writes:
I've researched a bit though I still haven't come up with a solution.
Since the problem lies within (the most simple) constructor, I tried to
modify it for another outcome. If I supplied a generic parameter to the
pre-constructor the "Cannot evaluate at compile time" message
disappeared but two new errors appeared instead.

This is what I modified:

this()(float x, float y, float z) => this(T)(float x, float y, float z)

If I use this code instead, I get two other errors appearing:

Error: template test.DVECTOR2.__ctor(T) does not match any function 
template declaration

This error and another one (individual to each statement) appears in the
following code statements:


Error: template test.DVECTOR2.__ctor(T) cannot deduce template function 
from argument types !()(float,float)
DVECTOR2 m_zoom = DVECTOR2(2f, 2f);

Error: template test.DVECTOR2.__ctor(T) cannot deduce template function 
from argument types !()(immutable(float),const(float))
immutable DVECTOR2 m_UP_DIR = DVECTOR2(0f, 1f, 0f);
Jul 04 2011
parent reply Ali =?iso-8859-1?q?=C7ehreli?= <acehreli yahoo.com> writes:
On Tue, 05 Jul 2011 02:44:03 +0200, Loopback wrote:

 I've researched a bit though I still haven't come up with a solution.
 Since the problem lies within (the most simple) constructor, I tried to
 modify it for another outcome. If I supplied a generic parameter to the
 pre-constructor the "Cannot evaluate at compile time" message
 disappeared but two new errors appeared instead.
 
 This is what I modified:
 
 this()(float x, float y, float z) => this(T)(float x, float y, float z)
 
 If I use this code instead, I get two other errors appearing:
 
 Error: template test.DVECTOR2.__ctor(T) does not match any function
 template declaration
 
 This error and another one (individual to each statement) appears in the
 following code statements:
 
 
 Error: template test.DVECTOR2.__ctor(T) cannot deduce template function
 from argument types !()(float,float)
 DVECTOR2 m_zoom = DVECTOR2(2f, 2f);
 
 Error: template test.DVECTOR2.__ctor(T) cannot deduce template function
 from argument types !()(immutable(float),const(float)) immutable
 DVECTOR2 m_UP_DIR = DVECTOR2(0f, 1f, 0f);
Here is a simple form of the same problem: struct S { this(T)(double d) {} } void main() { auto o = S(1.5); } Error: template deneme.S.__ctor(T) does not match any function template declaration Error: template deneme.S.__ctor(T) cannot deduce template function from argument types !()(double) The compiler is right: What should T be there? int? string? MyClass? I've realized again that I don't know how to specify the template parameter for the constructor. The following attempt fails as the compiler thinks S itself is a template: auto o = S!string(1.5); Error: template instance S is not a template declaration, it is a struct And if I try to be smart after the error message, this seg faults the compiler: auto o = S.__ctor!string(1.5); Ali
Jul 04 2011
parent reply Loopback <elliott.darfink gmail.com> writes:
On 2011-07-05 03:11, Ali Çehreli wrote:
 On Tue, 05 Jul 2011 02:44:03 +0200, Loopback wrote:

 I've researched a bit though I still haven't come up with a solution.
 Since the problem lies within (the most simple) constructor, I tried to
 modify it for another outcome. If I supplied a generic parameter to the
 pre-constructor the "Cannot evaluate at compile time" message
 disappeared but two new errors appeared instead.

 This is what I modified:

 this()(float x, float y, float z) =>  this(T)(float x, float y, float z)

 If I use this code instead, I get two other errors appearing:

 Error: template test.DVECTOR2.__ctor(T) does not match any function
 template declaration

 This error and another one (individual to each statement) appears in the
 following code statements:


 Error: template test.DVECTOR2.__ctor(T) cannot deduce template function
 from argument types !()(float,float)
 DVECTOR2 m_zoom = DVECTOR2(2f, 2f);

 Error: template test.DVECTOR2.__ctor(T) cannot deduce template function
 from argument types !()(immutable(float),const(float)) immutable
 DVECTOR2 m_UP_DIR = DVECTOR2(0f, 1f, 0f);
Here is a simple form of the same problem: struct S { this(T)(double d) {} } void main() { auto o = S(1.5); } Error: template deneme.S.__ctor(T) does not match any function template declaration Error: template deneme.S.__ctor(T) cannot deduce template function from argument types !()(double) The compiler is right: What should T be there? int? string? MyClass? I've realized again that I don't know how to specify the template parameter for the constructor. The following attempt fails as the compiler thinks S itself is a template: auto o = S!string(1.5); Error: template instance S is not a template declaration, it is a struct And if I try to be smart after the error message, this seg faults the compiler: auto o = S.__ctor!string(1.5); Ali
Hmm... Interesting. Thank you for clarifying and explaining that! I guess supplying T to the constructor when the parameters are already known to avoid compiler errors is not a solution then. Seems to me as if its only aggravates things. Though is there no solution nor any workarounds for this problem? I've attempted to use two different types of constructors and both appeared to be very limited, and I do not believe that this is the case. If you use a generic constructor is there no possible way to use it in cases where immutable and const is involved? Or is there a page that I have missed perhaps?
Jul 05 2011
parent reply Ali =?iso-8859-1?q?=C7ehreli?= <acehreli yahoo.com> writes:
On Tue, 05 Jul 2011 16:20:44 +0200, Loopback wrote:

 On 2011-07-05 03:11, Ali Çehreli wrote:
 struct S
 {
      this(T)(double d)
      {}
 }

 void main()
 {
      auto o = S(1.5);
 }

 Error: template deneme.S.__ctor(T) does not match any function template
 declaration
 Error: template deneme.S.__ctor(T) cannot deduce template function from
 argument types !()(double)

 The compiler is right: What should T be there? int? string? MyClass?

 I've realized again that I don't know how to specify the template
 parameter for the constructor. The following attempt fails as the
 compiler thinks S itself is a template:

      auto o = S!string(1.5);

 Error: template instance S is not a template declaration, it is a
 struct

 And if I try to be smart after the error message, this seg faults the
 compiler:

      auto o = S.__ctor!string(1.5);

 Ali
Hmm... Interesting. Thank you for clarifying and explaining that! I guess supplying T to the constructor when the parameters are already known to avoid compiler errors is not a solution then. Seems to me as if its only aggravates things. Though is there no solution nor any workarounds for this problem? I've attempted to use two different types of constructors and both appeared to be very limited, and I do not believe that this is the case.
I don't want to look like brushing off the problem but having many constructors make the code complicated. For example, it may be confusing which constructor gets called here: auto d = DVECTOR2(1.5); Are we setting just x or both x and y? Especially when we know that DVECTOR2 is a struct and that structs have this feature of assigning default values to the trailing unspecified members (at least in some cases), I think a factory function would be better in this case: auto d = square_DVECTOR2(1.5); Now we know that both x and y will be 1.5. Same can be said for some of the other constructors. It is not difficult at all for the caller to give us what we want; and it is clearer: D3DXVECTOR2 d3; // ... auto d = DVECTOR2(d3.tupleof); (I think this is in line with Kevlin Henney's "Parameterize from Above" guideline/pattern/idiom/etc. :))
 If you use a generic constructor is there no possible way to use it in
 cases where immutable and const is involved? Or is there a page that I
 have missed perhaps?
D2 has changed the meaning of inout to mean something like "templatize just the mutable/const/immutable qualification of the parameter" but it is not implemented fully yet. Look at "Inout Functions" on the Functions spec: http://d-programming-language.org/function.html
 struct DVECTOR2
 {
 	// Controls that the parameter is a valid type template Accepts
(T) {
 	enum Accepts = is(T == DVECTOR2) || is(T == float) || is(T ==
 	D3DXVECTOR2) || is(T == POINT); }
 	
 	// Whether the parameter is a float or not
  	template isScalar(T) { enum isScalar = is(T == float); }
 	
 	// The Variables
 	float x = 0f;
 	float y = 0f;
 
 	// Default Constructor
 	this()(float x, float y)
 	{
 		this.x = x;
 		this.y = y;
 	}
 
 	// Float Constructor
  	this()(float xy) { this(xy, xy); }
 
 	// Implement D3DXVECTOR2 and POINT support
       this(T)(T arg) if (Accepts!T
 	&& !isScalar!T) { this(arg.tupleof); }
Ali
Jul 05 2011
parent Loopback <elliott.darfink gmail.com> writes:
On 2011-07-05 18:05, Ali Çehreli wrote:
 I don't want to look like brushing off the problem but having many
 constructors make the code complicated. For example, it may be confusing
 which constructor gets called here:

      auto d = DVECTOR2(1.5);
That might be true. I just did what felt most convenient, but perhaps that is not always the solution.
 D2 has changed the meaning of inout to mean something like "templatize
 just the mutable/const/immutable qualification of the parameter" but it
 is not implemented fully yet. Look at "Inout Functions" on the Functions
 spec:
Foolish of me to forget about inout functions. Is there any possibility though that the inout tag offers a solution to my initial problem, where the constructor couldn't be evaluted at compile time? It feels a bit redundant if you would have to have unique constructors just to enable support for immutable instantiations of your class, or perhaps this lies within the use of templates and their generic parameters? I've been at this problem for over a day and it feels awful to be left with no choice and move away from using templates and instead having walls of boilerplate code just to support immutable and const instantiations of one's structure.
Jul 05 2011