Máté Cserép
October 2015, Budapest
In object-oriented languages constructors have the role to initialize newly created objects. Different styles are used including factory methods.
Structures/classes without constructors can be instantiated without constructor call. If at least one constructor is defined for a class, objects must be created through one of the constructors
// file: date.h
class date
{
friend bool operator<(date d1, date d2);
public:
// constructor
date(int y=2000, int m=1, int d=1) : year(y), month(m), day(d) { }
// explicit constructor
explicit date(const char *s);
private:
int year;
int month;
int day;
};
// question: use member or global operators?
bool operator<(date d1, date d2);
inline bool operator==(date d1, date d2) { return !(d1<d2 || d2<d1); }
inline bool operator!=(date d1, date d2) { return d1<d2 || d2<d1; }
inline bool operator<=(date d1, date d2) { return !(d2<d1); }
inline bool operator>=(date d1, date d2) { return !(d1<d2); }
inline bool operator>(date d1, date d2) { return d2<d1; }
// file: date.cpp
date:: date(const char *s)
{
sscanf(s, "%d.%d.%d", &year, &month, &day);
}
bool operator<(date d1, date d2)
{
return d1.year < d2.year || d1.month < d2.month || d1.day < d2.day;
}
// file: main.cpp
int main()
{
date d(2015, 3, 12);
if (d < 2010) { // works: implicit constructor
/* ... */
}
else if (d < "2015.1.12") { // does not work: explicit constructor
/* ... */
}
else if (d < date("2015.1.12")) { // works: explicit call of constructor
/* ... */
}
}
Named automatic object
Free store object
Non-static member object
Array element
Local static object
Global, namespace or class static object
Temporary object
Placement new
Free store object:
date *p = new date;
date *q = new date(*p);
date *s = new date[10];
delete p;
delete p; // undefined behaviour
delete s; // undefined behaviour
class list
{
public:
list();
~list();
/* ... */
private:
// The order is the relevant:
int id;
list *next;
list *prev;
};
// This will be automatically rearranged: id(nid), next(0), prev(0)
list::list() : id(nid), prev(0), next(0)
{ }
Creation of sub-objects:
struct X
{
X(int i)
{
x = i;
std::cerr << "X ctor, x = " << x << std::endl;
}
int x;
};
void f()
{
static X ix(0);
std::cerr << "f(), ix.x = " << ix.x << std::endl;
++ix.x;
if (ix.x > 5)
{
static X iy(ix.x);
static X iz(ix); // copy constructor
}
}
int main()
{
for ( int i = 0; i < 10; ++i) f();
return 0;
}
struct X { X(int i) { x = i; }; int x; };
struct Y { int y; };
int main()
{
const int n = 4;
int k = 4;
X x1[n]; // compile error: no default constructor
X x2[n] = { 1, 2, 3, 4};
Y y1[n];
Y y2[k]; // compile error: k is non-const
X *xp = new X[k]; // compile error: no default constructor
Y *yp = new Y[k];
std::vector<X> xv(10); // compile error: no default constructor
std::vector<Y> yv(10);
delete [] yp;
return 0;
}
struct Base
{
Base() { std::cout << "Base" << std::endl; }
virtual ~Base() { std::cout << "~Base" << std::endl; }
int i;
};
struct Derived : public Base
{
Derived() { std::cout << "Derived" <<std::endl; }
virtual ~Derived() { std::cout << "~Derived" << std::endl; }
int it[10]; // sizeof(Base) != sizeof(Derived)
};
int main()
{
Base *bp = new Derived;
Base *bq = new Derived[5];
delete bp;
delete[] bq; // this causes runtime error
return 0;
}
void f( string &s1, string &s2, string &s3)
{
const char *cs = (s1+s2).c_str();
cout << cs; // OK or bad?
if (strlen(cs = (s2+s3).c_str()) < 8 && cs[0] == 'a' ) // OK or bad?
cout << cs; // OK or bad?
}
Temporary objects:
// Bad!!
// Bad!!
// OK
void f(string &s1, string &s2, string &s3)
{
cout << s1 + s2;
string s = s2 + s3;
if (s.length() < 8 && s[0] == 'a' )
cout << s;
}
The correct way:
Another correct way:
void f(string &s1, string &s2, string &s3)
{
cout << s1 + s2;
const string &s = s2 + s3;
if (s.length() < 8 && s[0] == 'a' )
cout << s; // Ok
} // s1+s2 destroyes here, when the const ref goes out of scope
class Base
{
public:
virtual ~Base() { std::cout << "~Base" << std::endl; }
};
class Derived : public Base
{
public:
virtual ~Derived() { std::cout << "~Derived" << std::endl; }
};
int main()
{
Base *bp = new Derived;
delete bp;
return 0;
}
In most cases destructors should be virtual:
class Base
{
public:
Base() { std::cout << "Base defCtor" << std::endl; }
Base(const Base& rhs) { std::cout << "Base copyCotr" << std::endl; }
virtual Base* clone() { return new Base(*this); }
};
class Derived : public Base
{
public:
Derived() { std::cout << "Derived defCtor" << std::endl; }
Derived(const Derived& rhs) : Base(rhs)
{ std::cout << "Derived copyCotr" << std::endl; }
virtual Derived* clone() { return new Derived(*this); }
};
int main()
{
Base *bp = new Derived;
Base *bq = bp->clone();
return 0;
}
There is no virtual constructor, but we can simulate it:
class X
{
/* ... */
private:
X& operator=(const X&); // Disallow copying
X(const X&);
};
Forbid copying
Alternatively we can use the boost library
class X : boost::noncopyable // Disallow copying
{
/* ... */
};
Since C++11 deleted functions are also usable
class X
{
/* ... */
X& operator=(const X&) = delete; // Disallow copying
X(const X&) = delete;
};
class Y
{
/* ... */
Y& operator=(const Y&) = default; // Default copy semantics
Y(const Y&) = default;
};
Conversely, we can also say explicitly that we want to use the default copy behaviour:
class Foo
{
public:
Foo()
{
// code to do task A
}
Foo(int nValue)
{
// code to do task A
// code to do task B
}
};
In C++98/03, there are often cases where it would be useful for one constructor to call another constructor in the same class. Unfortunately, this is disallowed by C++03. Commonly this ends up resulting in either duplicated code:
class Foo
{
public:
Foo()
{
InitA();
}
Foo(int nValue)
{
InitA();
// code to do task B
}
void InitA()
{
// code to do task A
}
};
Alternatively we van use a init() non-constructor function to keep the common code accessible to both constructors that need it:
class Foo
{
public:
Foo()
{
// code to do task A
}
Foo(int nValue): Foo() // use Foo() default constructor to do A
{
// code to do task B
}
};
Fortunately C++11 adds the ability to chain constructors together (called delegating constructors):
While using the init() method is considered better practice than duplicating code, it has a couple of downsides:
In general, the C++ standard allows a compiler to perform any optimization, provided the resulting executable exhibits the same observable behaviour as if (i.e. pretending) all the requirements of the standard have been fulfilled. This is commonly referred to as the "as-if" rule.
The term return value optimization (RVO) refers to a special clause in the C++ standard that goes even further than the "as-if" rule: an implementation may omit a copy operation resulting from a return statement, even if the copy constructor has side effects.
struct C {
C() {}
C(const C&) { std::cout << "A copy was made." << std::endl; }
};
C f() {
return C();
}
int main() {
std::cout << "Hello World!" << std::endl;
C obj = f();
return 0;
}
The above example demonstrates a scenario where the implementation may eliminate one or both of the copies being made, even if the copy constructor has a visible side effect (printing text). The first copy that may be eliminated is the one where C() is copied into the function f()'s return value. The second copy that may be eliminated is the copy of the temporary object returned by f() to obj.
Depending upon the compiler, and that compiler's settings, the resulting program may display any of the following outputs:
Hello World! A copy was made. A copy was made.
Hello World! A copy was made.
Hello World!
May also optimize away move semantic!
#include <iostream>
#include <algorithm>
#include <chrono>
#include <stdlib.h>
#include <time.h>
struct Copyable
{
static const int SIZE = 10000;
Copyable()
{
data = new int[SIZE];
for(int i = 0; i < sizeof(data); ++i)
data[i] = rand();
}
virtual ~Copyable()
{
delete[] data;
}
Copyable(const Copyable& other)
{
data = new int[SIZE];
std::copy(other.data, other.data + SIZE, data);
}
Copyable& operator=(const Copyable& other)
{
if(this == &other) return *this;
delete[] data;
data = new int[SIZE];
std::copy(other.data, other.data + SIZE, data);
return *this;
}
protected:
int *data;
};
struct Movable
{
static const int SIZE = 10000;
Movable()
{
data = new int[SIZE];
for(int i = 0; i < sizeof(data); ++i)
data[i] = rand();
}
virtual ~Movable()
{
delete[] data;
}
Movable(const Movable& other)
{
data = new int[SIZE];
std::copy(other.data, other.data + SIZE, data);
}
Movable& operator=(const Movable& other)
{
if(this == &other) return *this;
delete[] data;
data = new int[SIZE];
std::copy(other.data, other.data + SIZE, data);
return *this;
}
Movable(Movable&& other)
{
data = other.data;
other.data = 0;
}
Movable& operator=(Movable&& other)
{
if(this == &other) return *this;
int *p = data;
data = other.data;
other.data = p;
return *this;
}
protected:
int *data;
};
template <class T>
T generate()
{
return T();
}
template <class T>
void swap_copy(T& a, T& b)
{
T t(a);
a = b;
b = t;
}
template <class T>
void swap_move(T& a, T& b)
{
T t(std::move(a));
a = std::move(b);
b = std::move(t);
}
template <class Function>
void measure(Function fun, const std::string& msg)
{
std::chrono::high_resolution_clock::time_point start, end;
start = std::chrono::high_resolution_clock::now();
fun();
end = std::chrono::high_resolution_clock::now();
auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(end - start).count();
std::cout << msg << ": " << duration << "ms" << std::endl;
}
void test_copy_return()
{
Copyable copy_obj;
for(int i = 0; i < 100000; ++i)
copy_obj = generate<Copyable>();
}
void test_move_return()
{
Movable move_obj;
for(int i = 0; i < 100000; ++i)
move_obj = generate<Movable>();
}
void test_copy_swap()
{
Copyable copy_obj_a, copy_obj_b;
for(int i = 0; i < 100000; ++i)
swap_copy(copy_obj_a, copy_obj_b);
}
void test_move_swap()
{
Movable move_obj_a, move_obj_b;
for(int i = 0; i < 100000; ++i)
swap_move(move_obj_a, move_obj_b);
}
int main()
{
srand (time(NULL));
measure(test_copy_return, "Copy return");
measure(test_move_return, "Move return");
measure(test_copy_swap, "Copy swap");
measure(test_move_swap, "Move swap");
return 0;
}
// > g++ move_rvo_test.cpp -std=c++11 -o prog.exe
// > prog.exe
// Copy return: 224ms
// Move return: 25ms
// Copy swap: 524ms
// Move swap: 2ms
// > g++ move_rvo_test.cpp -std=c++11 -o prog.exe -fno-elide-constructors
// > prog.exe
// Copy return: 500ms
// Move return: 33ms
// Copy swap: 522ms
// Move swap: 1ms