C. Keith Ray

C. Keith Ray writes about and develops software in multiple platforms and languages, including iOS® and Macintosh®.
Keith's Résumé (pdf)

Friday, April 16, 2010

Test-Driving C++

Reprinted from my blog in 2007.Jan.31 Wed

In most languages, when I'm using Test Driven Development to create a class, I only put into that class those methods or fields that I needed to pass a test. C++ has some exceptions to that, given how the compiler will generate aspects of a "canonical c++ class" for you.

I should explain the idea of a "Canonical C++" class. Imagine that I have this code:

class Buddy
{
public:
Icon* myIcon;
std::string myName;
};

Now, I didn't write a constructor, destructor, nor an assignment-operator, but the compiler did create those for me. It's as if I really wrote the following code:

class Buddy
{
public:
Icon* myIcon;
std::string myName;

Buddy() // default constructor
: myName() // invokes std::string's default constructor
{ // myIcon is not initialized, it probably has a garbage value here.
}

Buddy(const Buddy& other) // copy constructor
: myIcon( other.myIcon ) // copy the variable's value
, myName( other.myName ) // invokes std::string's copy constructor
{
}

~Buddy() // destructor
{
} // invokes std::string's destructor for myName.

Buddy& operator=(const Buddy& other)
{ // assignment operator
myIcon = other.myIcon; // copy the variable's value
myName = other.myName; // call std::string's assignment operator
}
};

A "canonical" C++ class has default constructor (and/or other constructors), copy constructor, destructor, and assignment-operator. These may be defined by the programmer or created by the compiler.

And this invisible compiler-generated code can be wrong, particularly if ownership of pointers or other resources is involved. Let's say that I test-drive a default constructor that sets up myIcon to point to a newly-created Icon object, and write the corresponding destructor code to delete the Icon object. It's hard to verify the "state" of an object after its destructor is called ('cuz it's GONE), but there are a few tricks to verify the behavior of a destructor that I won't get into here.

class Buddy
{
public:
Icon* myIcon;
std::string myName;

Buddy()
: myIcon( NULL )
, myName( "no name" )
{
myIcon = new Icon(Icon::DEFAULT_ICON);
}

~Buddy()
{
delete myIcon;
}
};

SPECIFY_(Context,BuddyHasDefaultNameAndIcon)
{
Buddy* aBud = new Buddy;
VALUE( aBud->myName ).SHOULD_EQUAL( "no name" );
VALUE( aBud->myIcon ).SHOULD_NOT_EQUAL( NULL );
delete aBud;
}

This test will pass. (by the way, I'm using "ckr_spec" here, a Behavior-Driven-Design framework I've written in C++ in my spare time. I'll publish more about ckr_spec one of these days.) However, this test doesn't exercise the compiler-created copy-constructor and assignment operators. AND THOSE ARE WRONG. Nothing (besides self-discipline) prevents anyone from writing the following (crashing) code:

void crashingCode()
{
Buddy keith;
Buddy keithClone(keith); // calls compiler-created copy constructor
Buddy anotherKeith;
anotherKeith = keith; // calls compiler-created assignment operator

// destructors are called invisibly here - crash deleting the same Icon
// object 3 times. (Also leaks an Icon object, too.)
}

The compiler-created copy constructors just copy the pointer to the Icon object. They don't create a NEW Icon object. So in "crashingCode" above, the Icon object created in the constructor of "keith" gets deleted three times, when the destructors for the "keith", "keithClone", and "anotherKeith" objects get called at the end of the function.

Therefore, when I'm test-driving a C++ class, very early on, I make a decision. Is this a "value" class that is going support the copy-constructor and assignment-operator, or an "entity" class that should never be copied because the instance represents something with a persistent identity? (These are some over-simplified ideas from Domain-Driven Design.) I can change my mind later, of course.

If my class is going to allow "value" semantics, then I'll need to write some tests to assure that the copy-constructor and assignment operator function correctly, whether I've written them, or the compiler has generated them.

If I'm not going to allow "value" semantics, then I need to signal to the compiler and to my fellow programmer not to generate or use the copy-constructor and assignment-operator. Declaring them private and unimplemented is how to do that.

class Buddy
{
public:
Icon* myIcon;
std::string myName;

Buddy()
: myIcon( NULL )
, myName( "no name" )
{
myIcon = new Icon(Icon::DEFAULT_ICON);
}

~Buddy()
{
delete myIcon;
}

private:
Buddy(const Buddy& other);
// don't implement copy constructor

Buddy& operator=(const Buddy& other);
// don't implement assignment operator
};
// the crashingCode example will not compile now.

For entity objects, quite often I don't want to allow the default constructor either, so I would declare that private and unimplemented as well.

The moral of the story is that in C++, sometimes you have to write code to prevent the compiler from writing the code for you. Just add that to your TDD/BDD development process.

No comments:

Post a Comment