Saturday, September 15, 2007

Named Constructors

Here is a little trick, that all you old-school C++ programmers might not know. Obviously it's called "Named Constructors".

So, suppose you need to make a 3x3 matrix class for your game-math library. You need it to make affine transformations so you have to make it represent rotation and scaling. So here is some code, that you'll probably write:


class matrix3x3
{
public:
   //scaling
   matrix3x3(float xscale, float yscale, float zscale);
   matrix3x3(float uniform_scale);

   //rotation
   matrix3x3(const vec3& axis, float angle);
   matrix3x3(float yaw, float pitch, float yaw);

   //other stuff
   //...
};


Great. But wait a minute! We have two constructors that take three floats each. Damn!

Obviously constructs like


matrix3x3 m(3, 1, 2); //wtf is this? Scaling?


...and the pure impossibility of having two constructors with the same signature make this a bad choice.

So, what can you do?
There is an old-school pattern to write external constructors. Like that:


void matrix_scaling(matrix& out, float xscale ...
void matrix_rotation_yaw_pitch_roll(matrix& out ...


That just doesn't look good. Luckily there is a thing called Return Value Optimization, and specifically Named Return Value Optimization or NRVO (try reading that with your mouth full). Basically NRVO is something that allows you to return heavy objects from functions without worying that they'll be copied, which is slow. And game developers don't like slow. They don't even know what slow means. It's either acceptable or not acceptable. Yeah!... Anywho, this is a link to a blog that explains just how cool NRVO is.

And how did we use it to create our named constructors? That's how:


class matrix3x3
{
public:
   static matrix3x3 scaling(float x, float y, float z)
   {
      matrix3x3 ret;
      //fill ret with appropriate stuff...
      return ret;
   }
   //you get the point...
   //...
   //you really get the point. there is no need for more examples.
};


And just look at how neat the code looks with the named constructors.


matrix3x3 m1 = matrix3x3::scaling(1, 2, 3);
matrix3x3 m2 = matrix3x3::rotation_yaw_pitch_roll(1, 2, 3);


Self documenting code at its finest. And no efficiency loss whatsoever.

I should mention that this is a relatively new standart feature and if you use retro compilers (less than vc8 or less than gcc4) you most probably will suffer from efficiency loss. So hurry and get something contemporary.

If you don't want to bother and read the link I posted above and just want to see whether your compiler has NRVO (I keep reading it "nervo" in my head... that's not right), just test this:


struct nervo //hehe
{
   nervo() : n(23) { cout << "hello world, baby" << endl; }
   nervo(const nervo& s) : n(s.n) { cout << "cloned!!!111one" << endl; }
   ~nervo() { cout << "lights getting dim..." << endl; }
   int n;
};

nervo test()
{
   nervo n;
   return n;
}

...

nervo x = test();

If there's no text like the one from the copy-constructor, you're good to go!

6 comments:

Parceval said...

This 'named constructors' looks just like static method that return object of the same class by value, to me.
So, any sane compiler should support them already.
Place the method in the header, and it will be inlined and copying will be optimized.
Besides, making matrices non-POD types is evil. Later, you will be unable to place them inside unions.
And initializing matrices should work perfectly via external functions, that can be named properly and will return matrices by value.

Unknown said...

Parceval, that's a bit like saying "this 'non-copyable' class looks just like a class that has private operator= and copy-constructor".
Actually the named construtors *are* static methods that return the object by value. NRVO helps us create them without actuall copying the object.
Also, static methods don't stop the class/structure from being a POD type.
And finally, named constructors make the code look cleaner for non-obvious constructions, and also not having to make all external functions friends of the object if they need to fill some of its private/protected data members.

Parceval said...

I still don't get that NRVO thing.
Current and past compilers understand static methods, successfully can inline them and optimize the copying.
Or this is just new terminology?

About POD - I said that, because of the initial idea of using constructors for POD type, since it started like that.

Unknown said...

Your sample code produces "cloned" message under VS 2005 and does not compile at all under gcc 4.1.1.

I suppose that one needs VS 2008 and gcc 5.x for it to work?

Arseny Kapoulkine said...

nervo::nervo(nervo&) is so wrong...

Unknown said...

zeux, thanks for that. I fixed it. It was a typo.

As for what compiler you need. VS2005 does support it, but the /02 option only in release (in debug it's /Od doesn't enable it). So you either must change your project settings, or compile in release mode.

Also gcc 4 does support it. It even will be enabled in a default compile "g++ nrvotest.cpp -o nrvotest".