Specific Constructors

Started by
8 comments, last by Aardvajk 17 years, 5 months ago
Is it possible to build constructors for structure and class types that are not [only] defined by the number and types of parameters sent to them? I'm only hinging the need for this based on the fact that constructors exist at all. What is their purpose, given a public access structure-based situation? I mean I can easily do this.. Vector a; a.AsCrossed( b, c ); as opposed to.. Vector a(b, c); I would like to do something whacky like this.. Vector a:AsCrossed(b,c); or Vector::AsCrossed(b,c) a;
Advertisement
I think I see what you're after, but I don't believe there's anything built into the language that supports it directly.

I do have a couple of other comments though. I used to think overloaded constructors were quite cool and convenient:
quaternion q(v1,v2);    // A quaternion to rotate from vector v1 to vector v2!matrix m(v1,v2);    // The tensor product of two vectors! Or maybe it's a matrix that rotates    // from v1 to v2. Hm...quaternion q(s, v);    // An axis-angle rotation! Or maybe we're just assigning initial values to the    // real and imaginary parts of the quat...who knows!matrix m(v);    // An orthonormal basis built around v! A non-uniform scale! The tensor product    // of v with itself! A translation matrix! It's anybody's guess!
Anyway, my conclusions are probably clear from the comments, but nowadays my feeling is that non-obvious or otherwise ambiguous overloaded constructors should be avoided. (Perhaps this is the motivation behind your 'named constructors' idea.)

I suspect though that the real answer to the question presented in your post is free global functions:
vector c = cross(v1,v2);quaternion q = rotation_arc(v1,v2);matrix m = tensor_product(v1,v2);
Or, if you prefer to use the copy constructor you could also use the following syntax:
vector c(cross(v1,v2));
It's not exactly what you're after, but it does offer both clarity and the convenience of single-line declaration and initialization (which seems to be the essence of your original example).

[Edited by - jyk on November 9, 2006 11:41:25 PM]
I care mostly about readability as well. But the code fitting on one line isn't an issue. I was more interested in the generated ASM. I'm not trying to optimize (if I can write readable code that performs an inkling better than other readable code, then I want to), but I've always had the impression that constructors save time when the initial value is known as it is declared.

I'm curious to know if there's any difference in the following situations (the end result values of c are identical):
Vector a=something,b=somethingelse;// Situation 1:Vector c;c.x = (a.y * b.z) - (a.z * b.y);c.y = (a.z * b.x) - (a.x * b.z);c.z = (a.x * b.y) - (a.y * b.x);// Situation 2:Vector c( (a.y * b.z) - (a.z * b.y),          (a.z * b.x) - (a.x * b.z),          (a.x * b.y) - (a.y * b.x) );// Situation 3:Vector c = Vector( (a.y * b.z) - (a.z * b.y),                   (a.z * b.x) - (a.x * b.z),                   (a.x * b.y) - (a.y * b.x) );

My (possibly false) impression was that 2 would be the least amount of work. I get that 3 makes a copy. But I'm not sure I understand why there's any difference between 1 and 2. Does the code always need to fill the stack data with a value, regardless of the validity of that value? Otherwise, I don't understand where additional work is needed between 1 and 2.

It doesn't really matter now, since there's no way to create such a readable constructor anyway. So I'm just asking out of curiosity. I'm also aware of how small of a difference there is, if one does exist. Just to again make it clear that I'm not optimizing.

Thanks [smile]
(EDIT: WRT performance, have you tried testing in release mode yet? It shouldn't make a difference. The compiler should be able to optimize away both a useless default-construction that's written over, and a useless copy-construction from a temporary, via inlining, if it's simple enough. IIRC, the compiler is allowed to assume your copy constructor just "works", and can optimize copies away as it likes - sometimes a subtle source of bugs: you can actually write code that works in release, but not in debug!)

Constructors exist to *initialize* things. This is a fundamentally different thing from assigning a value to an object, even if said assignment is "intended to initialize" the object - i.e. no matter how soon you assign after declaring the object, it still isn't an initialization.

Reasons why this is important:

1) You can deliberately declare non-default-constructible types; and then you have to specify a constructor when you declare them, for initialization.
2)
Quote:Original post by jyk
I suspect though that the real answer to the question presented in your post is free global functions:
vector c = cross(v1,v2);quaternion q = rotation_arc(v1,v2);matrix m = tensor_product(v1,v2);
Or, if you prefer to use the copy constructor:
vector c(cross(v1,v2));



The first three examples actually use the copy constructor rather than the assignment operator, because they are initializations. (You are responsible for writing things in such a way that this doesn't matter, at least from a correctness perspective.)

3) Constructors mean you *can't possibly forget* to initialize. In the cases where a default-constructed value is OK too, why burden yourself with remembering to call an init() member every time? You wouldn't do the same with the destructor, surely?



That said, you have at least two options.

1) You can just make use of the copy constructor, as jyk illustrated. Highly recommended versus the alternative:

2) You can pass a dummy parameter of a unique type, similar to how non-throwing new works:

struct Cross_t {} cross;struct Plus_t {} plus;class Vector {  Vector(const Vector& a, Cross_t&, const Vector& b) : /* initialization list */ {    /* anything you can't do via the initialization list */  }  Vector(const Vector& a, Plus_t&, const Vector& b) : /* initialization list */ {    /* anything you can't do via the initialization list */  }};Vector a(b, cross, c);Vector q(b, plus, c);


(You can *not* do it by template specialization on a dummy template type, because you'd have to explicitly specify the template type, but e.g. 'Vector<plus> a(b, c)' would provide a template parameter for the class, not the constructor-as-function.)
Thanks, Zahlman. You fully answered all of my questions. As long as the compiler is aware of such things, I'm totally comfortable.
Quote:Original post by Zahlman
Quote:Original post by jyk
I suspect though that the real answer to the question presented in your post is free global functions:
vector c = cross(v1,v2);quaternion q = rotation_arc(v1,v2);matrix m = tensor_product(v1,v2);
Or, if you prefer to use the copy constructor:
vector c(cross(v1,v2));

The first three examples actually use the copy constructor rather than the assignment operator, because they are initializations.
Oops, sorry about that - sloppy writing on my part. (I've edited my earlier post accordingly.)
Quote:Original post by Zahlman
WRT performance, have you tried testing in release mode yet?

I realized that I completely ignored this question. The answer is nope. I haven't done any testing regarding performance.

Is time-testing possible? I figured the difference would be so small, timing it would be impractical. And I doubt I could follow the ASM well enough to gain any knowledge, unless the different situations produce identical code. I can try that later.

Thanks again [smile]
Quote:Original post by Kest
I'm curious to know if there's any difference in the following situations (the end result values of c are identical):
Vector a=something,b=somethingelse;// Situation 1:Vector c;c.x = (a.y * b.z) - (a.z * b.y);c.y = (a.z * b.x) - (a.x * b.z);c.z = (a.x * b.y) - (a.y * b.x);// Situation 2:Vector c( (a.y * b.z) - (a.z * b.y),          (a.z * b.x) - (a.x * b.z),          (a.x * b.y) - (a.y * b.x) );// Situation 3:Vector c = Vector( (a.y * b.z) - (a.z * b.y),                   (a.z * b.x) - (a.x * b.z),                   (a.x * b.y) - (a.y * b.x) );



Note that situation 2 and situation 3 are identical. They will both construct c the same way. There is no copy constructor invoked (although in situation 3, one must be visible).

In situation 1, you have default-constructed a Vector and then modified it. This can be an expesive way to do things if object construction is expensive (for example, a memory allocation is required).

One important thing to consider when designing classes is the class invariant. I just finished trying to hack some legacy C-with-classes code, so this is at the top of my mind. If you can't find the class invariant for a class, you have not yet found the right design.

In situation 1 above, the class invariant is never violated, since a default-constructed Vector will have some value (although it might be arbitrary). For other classes, however, that might not be the case, so it's just a good habit to get into to make sure your objects are fully initialized by the end of their constructor and avoid the default-construct-then-initialize habit inherited from C-with-classes. I can't emphasize enough how it can aid reasoning about your objects later when someone is trying to support your code as legacy code (and that someone may be you).

Stephen M. Webb
Professional Free Software Developer

Quote:Original post by Bregma
In situation 1, you have default-constructed a Vector and then modified it. This can be an expesive way to do things if object construction is expensive (for example, a memory allocation is required).

I thought it was made clear that the compiler would do away with any overwritten values in situation 1. Will it not? The circumstances are usually very similar to what you see there, where it should be easy to detect.

Also, my vector class doesn't initialize values with the default constructor. You could Vector a(0,0,0) to init to a zero length vector. But the default costructor doesn't modify any data. Mostly do to the fact that a zero length vector can be just as dangerous as a random value. So in situation 1, no data values are assigned to the x,y,z components by my code. But I don't know if the compiler must do so anyway.

Quote:For other classes, however, that might not be the case, so it's just a good habit to get into to make sure your objects are fully initialized by the end of their constructor and avoid the default-construct-then-initialize habit inherited from C-with-classes.

That is understood. I wouldn't be bothered with construction performance in any complex class situations. But even some of my Vector functions themselves declare vectors in the same way as situation 1. So I was curious to know if there's any difference between a value given during contruction and a value given after construction. And what can I do to make sure the compiler can see that the code will be the same in either case. Again, performance is not important. But if it's trivial, then I would like to know about it.
Quote:Original post by Kest
Also, my vector class doesn't initialize values with the default constructor. You could Vector a(0,0,0) to init to a zero length vector. But the default costructor doesn't modify any data. Mostly do to the fact that a zero length vector can be just as dangerous as a random value. So in situation 1, no data values are assigned to the x,y,z components by my code. But I don't know if the compiler must do so anyway.


If your object is either created on the stack or created on the heap with new, the data members will contain random data until you explicitly initialise them, assuming they are not themselves class types with default constructors.

The danger is that between the declaration of the object and the initialisation code, you have an object in a state that does not conform to the invariants of the class. You, or others using your code, may at some point accidentally attempt to use a class in such a state.

I don't think the key issue with moving initialisation into a constructor really has much to do with performance or efficiency. Bregma said that method 1 could be expensive if construction is expensive. Clearly if construction is trivial, it will make little difference from an efficiency point of view, even notwithstanding optimisations. In my opinion initialising in the constructor has more to do with enforcing class invariants than performance or efficiency.

In this example, a vector initialised to zeros could be considered a null vector. I see what you mean that this could be as dangerous as random data, but similarly a null pointer can be as dangerous as a pointer to random memory, but at least can be identfied as a null pointer. The same would apply here I think.

This topic is closed to new replies.

Advertisement