(3) cpp abstractions more_on_user_defined_types

Embed Size (px)

Citation preview

Nico2014

Nico Ludwig (@ersatzteilchen)

(3) C++ Abstractions

TOC

(3) C++ AbstractionsAdvanced Creation of Instances on the Heap and the FreestoreThe Operators new/delete, new[]/delete[] and Placement

Controlling the Lifetime of an ObjectDestructors and Resource Acquisition is Initialization (RAII)

Copies of Objects and Avoiding Copying Objects

Instance- vs. Class Members

Sources: Bruce Eckel, Thinking in C++ Vol I

Bjarne Stroustrup, The C++ Programming Language

Instances of UDTs are passed by Value

As we learned, in C++ objects are getting passed by value to/from functions.This is also true for instances of UDTs.

We know that we can pass data by reference with pointers.It was required to do so, e.g. modify values of a variable passed to a function.

However, copying instances of a UDT can be costly, as all fields get copied.

Avoiding to copy instances of UDTs is another strength of C++.

C++ provides means to avoid copies of UDT-instances.We have to revisit dynamic creation of objects.

Creation of UDTs on the Heap

It's possible and often needed to create instances of UDTs on the heap like so:

That's boring! It's also cumbersome and dangerous!

Minor problems:We have to do the calculation of the size of the memory-block manually.

Major and dangerous problems:We can not call ctors during creation, instead we have to initialize explicitly!Dangers w/o ctors: forgetting to initialize, doing it twice or with erroneous values... ouch!

Is there a better way? Yes, the C++ freestore.

Date* date = static_cast(std::malloc(sizeof(Date))); // No surprise...date->day = 17; date->month = 10; date->year = 2012;date->Print();// >17.10.2012std::free(date);Which fields/functions of Date are public?

Creation and Deletion of UDTs on the Freestore

C++' freestore uses the operators new and delete to create/free dynamic objects.

Wow! The new-syntax!It allows creating dynamic instances w/o specifying the size of the memory block explicitly,

it allows calling a ctor with the object creation, so we have real initialization back

it saves us from forgetting ctor calls or doing "initialization" twice and as a "bonus"

it also works, when creating dynamic instances of fundamental type.

We have to use delete (not std::free()) to free an object from the freestore.

The operator delete can safely deal with 0-pointers.

Date* date = new Date(17, 10, 2012); // Create a Date instance on the freestore.date->Print();// >17.10.2012delete date; // Delete the Date instance from the freestore.In this course we make a distinction between heap and freestore. This distinction is not defined by the C++ standard. Instead it is used colloquially to separate the memory managed by std::malloc()/std::free() and new/delete, because they are assumed to be operating in different memory locations each.

The freestore impl. can be based on the heap impl., but not vice versa. This is a hint to the awaited performance: a faster impl. can not be based on a slower one.

Creation and Deletion of Arrays on the Freestore

We can also manage arrays on the freestore with new[] and delete[]:

The operator new[] is a little simplification over std::malloc() for arrays:We're no longer required to specify a size of the block. The count of elements will do.

But we can still not use an array initializer with new[], we have to assign explicitly!

We have to use delete[] (not delete) to free an array from the freestore.It's more complex than std::free(), which can be used w/ scalar objects and arrays.

Arrays created with new[] can not be freed with delete (w/o "[]") or std::free()!

The operator delete[] can safely deal with 0-pointers as well.

int* values = new int[2]; // Creation of an int array with two elements.values[0] = 22; // The operator new[] does not allow to initialize the elements, so wevalues[1] = 10; // have to assign them individually.delete [] values; // Free the memory occupied by values with delete[].C++11 uniformed initializers for arrays in the freestoreint* values = new int[2] {22, 10};

What if new or new[] fail?

If new or new[] fail, they will throw an std::bad_alloc exception. - Huh?

Exceptions are an advanced way to signal and handle run time errors.Exceptions can be caught and handled, or they can be ignored.

A fully ignored exception, won't be handled and leads to immediate program exit.

=> Let's make a contract:We will not handle std::bad_alloc exceptions thrown by new.If new failed, let the program exit immediately. That's the best we can do it for now.

We will not check the result of new against 0!

(The option new(std::nothrow) returns a 0-pointer on failure instead of throwing.)(We are not going to use the option std::nothrow for now!)

Extra: Placement-new

C++ allows creating objects with new on a non-freestore buffer:1. The operator new together with calling a ctor initializes the object.

2. The passed pointer to a buffer is used as location of the object.

=> This is called placement.

Example: creating an instance of a UDT with new on the heap.

The C++ placement-new syntax is simple to use:1. Include .

2. Allocate a buffer (from any memory) to place the to-be-created object in.

3. Pass a pointer to the allocated buffer to placement-new.

4. The buffer needs to be freed congruently (std::malloc() std::free()) with how it was created.

Placement-new is often used with special 3rd party buffers.

void* buffer = std::malloc(sizeof(Date)); // Create a blank buffer in the heap.// Create a Date instance with a ctor and place it on that blank buffer in the heap:Date* date = new (buffer) Date(17, 10, 2012); std::free(buffer); // Free date's buffer from the heap.

A Problem: Freeing dynamic Fields in UDT Instances

Hm... What's the problem with the UDT Person?When we create a Person object, a buffer for name needs to be created dynamically.

As we learned we are responsible for freeing dynamically created memory.

But who is responsible? The user/creator of a Person-instance?

When should the user free name? And how?

Freeing could be done explicitly as shown in the example above.

...but then freeing could be forgotten or done twice. Is there a better solution?

class Person { // Another UDT Personchar* name;public: Person(const char* name) {this->name = new char[std::strlen(name) + 1]; std::strcpy(this->name, name); } const char* GetName() { return this->name; }};void Foo() {// Create a Person object.Person nico("nico");std::coutname = new char[std::strlen(name) + 1]; std::strcpy(this->name, name); } ~Person() { // The dtor. delete[] this->name; }};void Foo() {// Create a Person object.Person nico("nico");std::coutPerson createdreturn;std::cout The pointer name does not represent the dynamic memory, it is only a pointer to the memory!if (handleName) { // Automatic - with RAII: Person nico("nico");//... doing some some stuff with nico} // The dynamic memory, encapsulated by the object nico, will be automatically freed, when nico// leaves its scope and is popped from the stack. On leaving the scope, Person's dtor will free the// dynamic memory automatically. The lifetime of the dynamic memory is now controlled by the stack.// => The object nico does itself represent and care for the dynamic memory encapsulated by its type.

Controlling Object Lifetime Unnecessary Object-Copies

When we return an object from a function, how often will it be copied?

What the heck is going on here?(1) First an anonymous object is created. ctor call.

(2) When we return an object from a function, it will be silently copied!

(3) The anonymous object will be destroyed. dtor call.

(4) When the copied object is assigned to nico it will be silently copied again!

(5) The copy created in (2) will be destroyed.

(6) nico will be destroyed.

Some of the copies are really unnecessary, but we can solve the problem...

PersonLitmus GetPersonByName(const char* name) {return PersonLitmus(name); // Creates an anonymous object.// >Person created (1) (2)} // >Person destroyed (3)if (handleName) { PersonLitmus nico = GetPersonByName("nico"); // >Person destroyed (4) (5)} // >Person destroyed (6)By default most C++ compilers are able to create code that suppresses the generation of copies on returning object. - This feature is called Return Value Optimization (RVO). RVO has been disabled in this example to unleash the full story of copied objects (gcc-option: -fno-elide-constructors).

The shown silent copies are not created with the ctor accepting a const char*, and also mind that there exists no dctor in PersonLitmus. The copies are created by the automatically provided "copy constructor". We'll learn about the copy constructor in a future lecture.

Controlling Object Lifetime Automatic Arrays revisited

For matters of completeness we'll analyze how arrays work with RAII.

On destruction, the dtors of all objects in the array will be called.Mind that dtors for the anonymous copies are called as well.

Therefor we call them automatic arrays (in opposite to dynamic arrays):the lifetime of an automatic array's objects is bound to the array's scope.

void Foo() {PersonLitmus persons[] = { PersonLitmus("trish"), PersonLitmus("john") };// >Person created// >Person created}// >Person destroyed // anonymous copies.// >Person destroyed// >Person destroyed // the array elements// >Person destroyed

Controlling Object Lifetime Avoiding Object-Copies

We can get rid of the superfluous copies by using the freestore.The stack manages objects by scope automatically, it also creates the extra copies.

But here we as programmers have to reclaim the control of an object's lifetime!

So we have to make an objects lifetime independent from the scope of its variable.

We learned that we can control the lifetime of objects created in the dynamic memory.

=> We have to create a PersonLitmus in the freestore with the new operator.We will also call the ctor by using the new operator.

If the object's lifetime is independent of its scope, when will it be freed?

=> Of course with delete! Mind that we are responsible for dynamic memory!Calling delete will also call the dtor.

PersonLitmus* GetPersonByName(const char* name) {return new PersonLitmus(name); // Creates an anonymous object.// >Person created}if (handleName) { PersonLitmus* nico = GetPersonByName("nico");delete nico;} // >Person destroyed

Extra: Using Resources temporarily

Often we have just to allocate a resource, use it and then deallocate it.Let's assume a member function to give a phone call to a PersonLitmus:

And we can "use" a PersonLitmus resource like so:

When we create objects on the freestore, we have to free explicitly.And this explicit freeing is very often forgotten leading to memory leaks...

C++ provides a special tool to simplify such situations: auto pointers.

PersonLitmus* nico = new PersonLitmus("nico");// >Person creatednico->GiveACall();// >ring ringdelete nico;// >Person destroyedvoid PersonLitmus::GiveACall() {std::coutPerson creatednico->GiveACall();// >ring ring}// >Person destroyed (std::auto_ptr's RAII is effective here)C++11 std::unique_ptr{std::unique_ptr nico(new PersonLitmus("nico"));// >Person creatednico->GiveACall();// >ring ring}// >Person destroyed // std::unique_ptr's RAII is effective hereSometimes auto pointers are called smart pointers, but C++' std::auto_ptr is not very smart.

std::unique_ptr does also support dynamic arrays created in the freestore.

Summary: Controlling Object Lifetime

Dealing with objects created on the stack (autos):We've to use "complete" objects on the stack, which will lead to silent copies.

We've less control over an object's lifetime, RAII conveniently binds lifetime to scopes.

Dealing with dynamically created objects:We have to use pointers; function interfaces need to use pointers as well.The objects will not be copied! - We'll only pass a shortcut the object around.

We have full control over an object's lifetime, but also full responsibility for the lifetime.

The operators new and delete conveniently work with ctors and the dtor.

=> C++ programmers decide, whether objects are created on stack or freestore.On many other languages/platforms a type declares, where it is created (e.g. C#/.Net).

We can also create objects on the heap (std::malloc()/placement-new/std::free()).But then we have to call construction functions and the dtor explicitly.

Having a "complete" object on the stack means that all fields of the object will be stored on the function's stack. This is typically the situation, where PODs are used.

The Lifetime of static/global Objects

The ctors of global objects are called on the start of the program.In a translation unit (tu) the initialization order is from top to bottom.

Between tus the order is not defined, but often it depends on the link order.Link order: the sequence, in which o-files are being passed to the linker.

The ctors of other static objects, e.g. local statics, can be deferred.

The dtors of globals and other static objects are called when the programs ends.The dtors of globals are called in the opposite order to initialization in a tu.

Between tus the order is not defined, but often it depends on the link order as well.

Instance independent Functionality

Sometimes we need UDT-utility-functions that are independent of an instance.Often such functions end in free functions, i.e. not as member functions, e.g. like so:

Features of the free function Today():It is independent of a concrete instance of the type Date.

But it creates a new Date-instance!

The idea of having free utility-functions is not bad, but:the belonging to to a type is not given, but Today() somehow does depend on Date,

and we can do better...

Date Today() { // Today() returns the current date in a Date object. std::time_t t = std::time(0); // Get the current time (API from ). std::tm* now = std::localtime(&t); return Date(now->tm_mday, 1 + now->tm_mon, 1900 + now->tm_year);}Date now = Today();now.Print();// >20.12.2012

Static Member Functions

We can bind free functions to a type: static member functions.

The static member function Date::Today() has following features:it's independent of a concrete Date instance (no Date object is needed to call Today()),

because it is public, it can be called from outside the class.

Syntactic peculiarities of static member functions: In the definition of a static member function, the keyword static is used

and static member functions are called with the :: operator applied on the type name.

class Date { // (members hidden)public:static Date Today() { // Today() as static member function. std::time_t time = std::time(0); std::tm* now = std::localtime(&time); return Date(now->tm_mday, 1 + now->tm_mon, 1900 + now->tm_year);}};Date now = Date::Today();now.Print();// >20.12.2012

The static keyword must not be repeated on a non-inline definition.

More about static Members

We can also define static fields in a UDT.(But non-integral, non-constant static fields can not be defined inline!)

All static (non-private) members can additionally be accessed via instances:

Common misunderstandings according static members:They have nothing to do with the static (i.e. global) storage class.

We can't have a this-pointer in static member functions, because there's no instance.

class Date { // (members hidden)public: // seconds per gregorian year:static const double secondsPerGregorianYear;};// The definition of secondsPerGregorianYear has to be non-inline.const double Date::secondsPerGregorianYear = 31556952.2;

std::cout