Namespaces
Variants
Actions

Structs and Classes

From cppreference.com
Revision as of 08:49, 18 October 2013 by FJW (Talk | contribs)


Let's assume we want to calculate the distance between two points in space; the formula for this is quite simple: Sum the squares of the distances in every dimension and take the square-root:

distance = sqrt(abs(x1-x2)² + abs(y1-y2)² + abs(z1-z2)²)

Since this is somewhat heavy to write every time, we'll use a function for that:

#include <iostream>
#include <cmath> // needed for sqrt() and abs()
 
double square(double number)
{
    return number * number;
}
 
 
double distance(double x1, double y1, double z1, double x2, double y2, double z2)
{
    auto squared_x_distance = square(std::abs(x1-x2));
    auto squared_y_distance = square(std::abs(y1-y2));
    auto squared_z_distance = square(std::abs(z1-z2));
    auto sum = squared_x_distance + squared_y_distance + squared_z_distance;
    return std::sqrt(sum);
}
 
int main()
{
    std::cout << "The points (0,1,2) and (4,1,0) have the distance "
        << distance(0,1,2,4,1,0) << '\n';
}

Output:

The points (0,1,2) and (4,1,0) have the distance 4.47214

The solution is working, but if we are honest, it isn't really nice: Passing the points into the function by throwing in six arguments is not only ugly, but also error-prone. Luckily C++ has solutions for this: Structs and classes. The biggest difference between these two is conventional, not technical, so we can look into them together.

A struct is basically a collection of values. In our example a point is represented by three doubles which even got implicit names: x, y, and z. So let's create a new type that is exactly that:

#include <iostream>
#include <cmath> // needed for sqrt() and abs()
 
struct point {
    double x;
    double y;
    double z;
};
 
double square(double number)
{
    return number * number;
}
 
double distance(const point& p1, const point& p2)
{
    auto squared_x_distance = square(std::abs(p1.x-p2.x));
    auto squared_y_distance = square(std::abs(p1.y-p2.y));
    auto squared_z_distance = square(std::abs(p1.z-p2.z));
    auto sum = squared_x_distance + squared_y_distance + squared_z_distance;
    return std::sqrt(sum);
}
 
int main()
{
    std::cout << "The points (0,1,2) and (4,1,0) have the distance "
        << distance(point{0,1,2}, point{4,1,0}) << '\n';
}

Output:

The points (0,1,2) and (4,1,0) have the distance 4.47214

Reducing six arguments to two, which in addition share semantics is clearly an improvement. It is obvious that the code got way cleaner.

Construction

Above we created our points by writing point{0,1,2}. This worked because point is an extremely simple structure. In general (we'll discuss the exact circumstances later) we need to implement the initialization ourself though.

Considering our current struct: Leaving variables uninitialized is evil and there is no exception for variables in structs and later on classes. So let's make sure, that they are zero, unless explicitly changed:

#include <iostream>
 
struct point {
    // this makes sure that x, y and z get zero-initialized 
    // at the construction of a new point:
    double x = 0.0;
    double y = 0.0;
    double z = 0.0;
};
 
int main()
{
    // no longer possible:
    // auto p = point{1,2,3};
 
    // this has always been possible, but dangerous 
    // now it's safe thanks to zero-initialization:
    point p1;
 
    // this is exactly the same as above:
    point p2{};
 
    std::cout << "p1: " << p1.x << '/' << p1.y << '/' << p1.z << '\n';
    std::cout << "p2: " << p2.x << '/' << p2.y << '/' << p2.z << '\n';
}

Output:

p1: 0/0/0
p2: 0/0/0

This works but we lose the great advantage of initializing a point with the values we want in a comfortable way. The solution to this is called a constructor. It is a special function that is part of a struct and is called when the object is created.

Let's create one that behaves like the one we had in the beginning:

#include <iostream>
 
struct point {
    // a constructor has neither returntype nor is it
    // possible to return a value from it. Aside from that,
    // it's name is identical with that of it's class:
    point(double x_arg, double y_arg, double z_arg)
    {
        // we can access all member-variables of the struct 
        // inside the constructor:
        x = x_arg;
        y = y_arg;
        z = z_arg;
    }
 
    double x = 0.0;
    double y = 0.0;
    double z = 0.0;
};
 
int main()
{
    // now these constructions work again:
    point p1{1,2,3};
    auto p2 = point{4,5,6};
 
    std::cout << "p1: " << p1.x << '/' << p1.y << '/' << p1.z << '\n';
    std::cout << "p2: " << p2.x << '/' << p2.y << '/' << p2.z << '\n';
}

Output:

p1: 1/2/3
p2: 4/5/6

If we look at the code, we see a very common situation: We have several data-members in our struct, one argument for each of them, and we directly assign the value of the argument to the member. This is fine, if the members are just doubles or ints, but it can create quite an overhead, if the default-construction of the member (which must be completed upon entry of the constructor) is expensive, like for std::vector. To solve this problem, C++ provides a way to initialize data-members before the actual constructor-body is entered:

#include <iostream>
 
struct point {
    // the members x, y and z are intialized with the arguments x, y, and z:
    point(double x, double y, double z) : x{x}, y{y}, z{z} 
    {} // <- the actual body is now empty
 
    double x = 0.0;
    double y = 0.0;
    double z = 0.0;
};
 
int main()
{
    point p{1,2,3};
    std::cout << "p: " << p.x << '/' << p.y << '/' << p.z << '\n';
}

Output:

p: 1/2/3

This way of initializing members is almost always preferable if it is reasonably possible. It should however be noted, that there is one danger using it: The member-variables are initialized in the order of declaration in the class, not in the order of the initialization, that the constructor seems to apply. As a result the following code is wrong:

struct dangerous_struct {
   
    // undefined behavior: var1 gets initialized before var2.
    // -> var2 is read before initialized
    dangerous_struct(int arg) : var2{arg}, var1{var2}
    {}
   
    int var1;
    int var2;
};

int main()
{
    dangerous_struct x{1};
}

Note however, that it is allowed to initialize data-member from already initialized other data-members.