7/9
5. Objects
Objects are generic programming elements containing members.
-
Members can be data : they are called attributes
-
or functions: they are called methods or member functions.
In C++ members can be qualified by public or private [1].
Public members can be accessed anywhere in the program using the syntax objectName.member. They are used to define the public API of the object.
Private members cannot be accessed from outside the class definition. They can model hidden internal state of the object that should be viewed, nor modified, outside of object internal functions.
In C++ objects can be defined using the keywords struct or class. Struct and class have identical behavior, at the exception that struct members are public by default, while class members are private by default.
Common practice usually use struct for lightweight objects with public API (ex. 3D vector where we can access the attributes x, y, and z). At the opposite, class are usually used for heavier objects with internal private state that are not supposed to be publically exposed (ex. Class modeling a window).
Example of object
Let us consider an example of struct modeling a 3D vector called vec3 containing 3 floating values (x,y,z).
//definition of the object
struct vec3
{
float x;
float y;
float z;
};
int main()
{
// creation of vec3 with coordinates (1, 2.5, 3.5)
vec3 p {1.0f, 2.5f, 3.5f};
// attributes can be accessed by their name
p.y = -4.5f;
p.z = p.x - p.y;
// objects can be const-qualified (ensure that their attributes cannot be changed)
const vec3 p2 {5.0f, 1.5f, 3.5f};
return 0;
}
or, similarly using the keyword class
class vec3
{
// public keyword is necessary to be able to access attributes outside the class definition.
public:
float x;
float y;
float z;
}
-
vec3 is the object (correspond to the C++ variable type), p and p2 are called instances of this object.
-
Objects are, by default, created on the heap memory in C++ (fast creation, access, and automatic destruction at the end of block). [2]
-
Attributes are contiguously stored in memory. In this case, the three float of vec3 are continuous in memory leading to an object of 3 \(\times\) sizeof(float) = 12 bytes.
-
Attributes of objects can be other objects.
Note on memory layout
Combination of std::vector (or std::array) with object allows to handle contiguous buffers of bytes.
For instance std::vector<vec3> can be viewed at high level as a sequence of 3D positions, but also at a lower level as a contiguous buffer of float values. This allows to manipulate data at high level (sequence of 3D position), while being compatible with efficient low level C calls (buffer of floats).
This can be typically be used in efficient computer graphics code requiring inputs given as contiguous buffer of bytes.
Functions on objects
Functions can be defined to take objects as arguments.
vec3 addition(const vec3& a, const vec3& b)
{
return vec3{a.x+b.x, a.y+b.y, a.z+b.z};
}
void display(const vec3& v)
{
std::cout << v.x << "," << v.y << "," << v.z << std::endl;
}
int main()
{
const vec3 p1 {1.0f, 2.5f, 3.5f};
const vec3 p2 {2.0f, -1.0f, 1.5f};
const vec3 p3 = addition(p1,p2);
display(p3); // print 3.0, 1.5, 5.0
return 0;
}
Note that object should usually be passed as argument of function as reference (const reference when not modified within the function) to avoid useless data copy.
Methods
Methods are similar to functions defined inside the object.
#include <iostream>
//definition of the object
struct vec3
{
float x;
float y;
float z;
vec3 addition(const vec3& b) const;
void display() const;
};
vec3 vec3::addition(const vec3& b) const
{
return vec3{x+b.x, y+b.y, z+b.z};
}
void vec3::display() const
{
std::cout << x << "," << y << "," << z << std::endl;
}
int main()
{
const vec3 p1 {1.0f, 2.5f, 3.5f};
const vec3 p2 {2.0f, -1.0f, 1.5f};
const vec3 p3 = p1.addition(p2);
p3.display(); // print 3.0, 1.5, 5.0
return 0;
}
Methods are called on the object using the syntax object.method(arguments).
Note that the signature of methods in the implementation slightly differs from functions
vec3 vec3::addition(const vec3& b) const
-
vec3::indicates that we define a method on the vec3 object (:: is the scope resolution operator) -
the last
constindicates that this method doesn’t modifies the attributes of the object. Similarily to const variables, const methods should be used by default. Non const methods should only be used when attributes are modified. -
Within the implementation, you may use any attribute of method of the current object (here x, y, and z).
C++ let the programmer free to choose between methods and functions. This choice is usually a matter of design.
When dealing with multiple files, class definition must be written in header files, while the implementation of methods and functions are written in .cpp files.
Inheritance
Inheritance consists in defining an object as a sub-type of another one. Inherited objects have access to public members of their base object, and can further define new members.
Inheritance can typically be used to model specialized behavior from general category of objects.
ex. Modeling elements on a map
// Base object, everything have a position on a map
struct element_on_map
{
float latitude;
float longitude;
};
// Inherited object
// A house in an element on map and contains other attributes (postal address, population, etc)
struct house : element_on_map
{
int population;
std::string address;
};
// Other inherited object
// A tree in an element on map and contains other attributes (type of tree, etc)
struct tree : element_on_map
{
std::string type;
};
-
Inheritance is defined using the statement
struct inheritedType : baseType -
When inheriting class, public keyword must be explicitly used to indicate public inheritance
class inheritedType: public baseType
Inheritance vs aggregation
Inheritance and aggregation (object containing another object) can sometimes be counfounded. Good practices suggest that inheritance should answer the question This inheritedObject is a type of baseObject. Otherwise, prefers to use aggregation.
Note also that inheritance can only extend properties of a baseClass, but not restrict them (ex. a square primitive has less degrees of freedom than a rectangle primitive. Thus a square geometry may not be efficiently modeled as inheriting from a rectangle geometry).
Constructor and destructors
We call constructors methods called to initialize attributes of the objects when instantiated.
Default constructors
Objects with public attributes have default compiler-generated constructors setting values of attributes in the order of their declaration.
#include <iostream>
struct vec3
{
float x;
float y;
float z;
};
void display(const vec3& p)
{
std::cout<< p.x<<","<<p.y<<","<<p.z<<std::endl;
}
int main()
{
// built-in empty constructor
// no initialization => undefined values in p1
vec3 p1;
display(p1);
// built-in constructor with attribute initialization
vec3 p2 {5,-4,7};
display(p2);
// same than previous
auto p3 = vec3{5,-4,7};
display(p3);
return 0;
}
Note that the first instanciation doesn’t initialize attributes of the struct. This leads to undefined values (and thus undefined behavior if you use these values). To avoid such behavior, you can define default value in the object definition
---
struct vec3
{
float x = 0; // defaults values for x, y, and z
float y = 0;
float z = 0;
};
---
User defined constructor
User can define its own constructor behavior.
Let us consider that you want to define the behavior such as vec3 p(5.0f) initialize all coordinates to 5.
You may define
struct vec3
{
vec3(float value);
float x;
float y;
float z;
};
vec3::vec3(float value)
:x(value),y(value),z(value)
{
}
And use it the following way
vec3 p1 {5.0f};
// or
vec3 p2 (5.0f);
-
Constructor implementations can use the special syntax
:attribute1(value1), attribute2(value2), …to initialize their attributes. This is called initializer list, and allows to set the values of the attributes before any other initialization.
In this case, the syntax of initializer list is similar to set in the function body
vec3::vec3(float value)
{
x = value;
y = value;
z = value;
}
-
User defined constructors can use brackets
{…}or parenthesis(…)to send their argument when called.
When at least one constructor is defined by the user, the compiler doesn’t generate built-in ones.
Therefore, to use
-
vec3 p;you must explicitly definevec3::vec3(). -
vec3 p {x,y,z};you must explicitly definevec3::vec3(float x, float y, float z).
Exemple of complete constructor definition
#include <iostream>
struct vec3
{
vec3();
vec3(float value);
vec3(float x, float y, float z);
float x;
float y;
float z;
};
vec3::vec3()
:x(0.0f), y(0.0f), z(0.0f)
{}
vec3::vec3(float value)
:x(value),y(value),z(value)
{}
vec3::vec3(float x_arg, float y_arg, float z_arg)
:x(x_arg), y(y_arg), z(z_arg)
{}
void display(const vec3& p)
{
std::cout<< p.x<<","<<p.y<<","<<p.z<<std::endl;
}
int main()
{
vec3 p1; // no argument - use vec3::vec3()
display(p1);
vec3 p2 {5.0f}; // one argument - use vec3::vec3(float value)
display(p2);
vec3 p3 {4.0f,7.0f,-4}; // three arguments - use vec3::vec3(float x, float y, float z)
display(p3);
return 0;
}
Copy constructor
Assigning an object as a copy of another one is also automatically generated by the compiler. By default, all attributes are copied from source to destination. This is called copy constructor. You may define its behavior if needed.
Example of use of copy constructor
#include <iostream>
struct vec3
{
vec3(float x, float y, float z);
// Copy constructor
vec3(const vec3& source);
float x;
float y;
float z;
};
vec3::vec3(const vec3& source)
:x(source.x), y(source.y), z(source.z)
{
std::cout<<"Calling copy constructor"<<std::endl;
}
vec3::vec3(float x_arg, float y_arg, float z_arg)
:x(x_arg),y(y_arg),z(z_arg)
{
}
void display(const vec3& p)
{
std::cout<< p.x<<","<<p.y<<","<<p.z<<std::endl;
}
int main()
{
vec3 p1 {1.0f, 2.0f, 3.0f};
display(p1);
vec3 p2 = p1; // copy constructor will be called
return 0;
}
Note that copy constructor will be called when passing object to function as a copy.
Destructors
Destructor is called when objects are removed from memory. At the opposite of other methods, destructors shouldn’t be called explicitly, but are automatically called by the system - typically when the object reaches the end of the block in which it was declared.
#include <iostream>
struct vec3
{
//destructor
~vec3();
float x;
float y;
float z;
};
// destructor implementation
vec3::~vec3()
{
std::cout<<" [ Destructor is called ] "<<std::endl;
}
void display(const vec3& p)
{
std::cout<< p.x<<","<<p.y<<","<<p.z<<std::endl;
}
int main()
{
{
vec3 p1 {1.0f, 2.0f, 3.0f};
std::cout<<"Destructor on p1 should be called after this line"<<std::endl;
} // call destructor on p1
std::cout<<"p1 should have been destroyed"<<std::endl;
return 0;
}
Default destructor doesn’t perform any action (works fine on heap allocated attributes).
General practices on constructors/destructors
If your expected behavior corresponds to compiler generated constructors and destructor, avoid defining custom ones. Default compiler-generated constructors and destructors are efficient and safely implemented, while user one may suffer from bugs (ex. forget to initialize an attribute added in the class afterward, etc.).
Exercise
Memory layout
Consider the following objects
struct A
{
int a;
float b;
double c;
};
struct B
{
std::array<float,3> position;
std::array<float,3> color;
std::array<float,2> texture-coordinates;
};
struct C
{
B first_vertex;
B second_vertex;
};
-
What is the size in memory took by these objects. Draw a schema of how the memory is organized.
Modeling an object "straight line"
Consider a 2D straight line defined by the equation \(a\,x+b\,y+c=0\). The coefficient \((a, b, c)\) are stored in an object line.
struct line
{
float a;
float b;
float c;
// Constructors to be defined
// ...
};
line can be constructed either with no parameter (horizontal line passing by the origin), or in defining directly the coefficient \((a, b, c)\), or in defining two arbitrary (non counfounded) 2D points \(p_0\) and \(p_1\) belonging to this line.
-
Implement this object and its constructors. Declare the object in a header file, and implement constructors in a separate .cpp file.
-
Implement the function intersect computing the intersection between two lines passed as argument, and returning the 2D coordinates of the intersection.
We will suppose that you can model the 2D points by the object
struct vec2
{
float x;
float y;
};