.. Copyright (C) Dave Parillo. Permission is granted to copy, distribute and/or modify this document under the terms of the GNU Free Documentation License, Version 1.3 or any later version published by the Free Software Foundation; with Invariant Sections being Forward, and Preface, no Front-Cover Texts, and no Back-Cover Texts. A copy of the license is included in the section entitled "GNU Free Documentation License". .. index:: single: constructors Constructors ============ All user defined types include a constructor. Even if you do not write one explicitly, the compiler will write one for you. For most of the classes and structs seen so far, we let the compiler generate the default constructors for us. ==================== ======================= Operation Function Signature ==================== ======================= default constructor ``X()`` copy constructor ``X(const X&)`` copy assignment ``operator=(const X&)`` move constructor ``X(X&&)`` move assignment ``operator=(X&&)`` destructor ``~X()`` ==================== ======================= default constructor Create a new object in 'the default way'. If you do not write one, the compiler will unless you write your own overloaded constructor. The default constructor can be overloaded like any other function. When you design your types, you may create constructors with one, two, or more arguments as appropriate. copy constructor Create a new object by copying an existing object. copy assignment Replace the state of an existing object using another object. move constructor Added in C++11. Like copy construction, but the existing source object is invalidated. The state is *moved* from the source object when the new object is constructed. move assignment Added in C++11. Like copy assignment, but the existing source object is invalidated. The state is *moved* from the source object when the object state is transferred. Move assignment and move constructors are optional. If you do not define them, the compiler will fall back to the copy constructor and copy assignment instead. However, move semantics may make your code more efficient by replacing potentially expensive copy operations with faster alternatives. destructor A *destructor* is a special function that is **always** called when an object goes out of scope. The purpose of a destructor is to reclaim any memory or perform any other cleanup tasks that need doing before the object is finally destroyed. A few points about destructors: - Like constructors, destructors have no return type. - Destructors take no arguments, so they never have a parameter list. There is no way to overload a destructor. - We don't call this function ourselves, we leave that to a program as it runs. We will explore destructors more when we explore containers. For now, just remember that a destructor always exists for every class whether you write one or not and it is always called when the class is destroyed. The compiler will **not** generate a default constructor if we write any non-default ones. The compiler may still create other default operations. So while this is fine: .. tb-code:: cpp struct point { int x = 0; int y = 0; }; int main () { point p; // implicitly defined constructor [[maybe_unused]] point q = p; // implicitly defined copy constructor } If the only constructor available is a default constructor, then we can only initialize our point using the default constructor. .. tb-code:: cpp :compileargs: ['-std=c++11'] struct point { int x = 0; int y = 0; }; int main () { // this works, but it's tedious point p; p.x = 8; p.y = 13; // This syntax is preferred, but won't compile in C++11 point e {3,5}; } As a general rule, if we want to initialize class members at construction time, then we need to add custom constructors. .. tb-code:: cpp :compileargs: ['-std=c++20'] struct point { int x; int y; point (int x, int y) : x(x), y(y) {} }; int main () { point p {3,5}; // 2 arg constructor OK point q = p; // default copy constructor OK point e; // compile error } Now we can use our 2 argument constructor, but now our old default invocation is broken. Why? Because whenever you write a non-default constructor the compiler will no longer write a default constructor for you. This is fixed by either: - Writing our own default implementation .. code-block:: cpp point () : x(3), y(5) {} - Telling the compiler to write it .. code-block:: cpp point () = default; - Telling the compiler to delete it .. code-block:: cpp point () = delete; In the ``= delete`` case, attempting the use the default constructor is still a compile error, but the error is more explicit: you can't use the default constructor because it was deleted. The compiler will provide a clear error message saying so. .. important:: If you write a non-default constructor, then you should *always* write your own default constructor, or explicitly instruct the compiler to make it for you, or delete it. In general, take care deleting the default constructor. Delete it only when you are sure objects of the class will **never** need to be default constructed or that there is simply no sensible set of defaults for the class. Initialization syntax --------------------- Some programmers coming to C++ from other OO languages sometimes feel as if they have to initialize objects like this: .. code-block:: cpp point p(); Even though all semester, you been writing: .. code-block:: cpp std::string s; std::vector v; std::random_device r; When it comes to user defined types, sometimes it feels 'incomplete' if you don't include the ``()``. Usually, those parentheses create more problems than they resolve. This is because of an inherent ambiguity in the C++ language. Although it seems obvious to us the statement ``point p();`` is a call to the default constructor and the results should be a new variable ``p``, the compiler interprets it differently. The basic rule is: .. code-block:: none If it looks like a function call, it's a function call. This means that in the code above, the compiler instead looks for: - a function named ``p`` - that takes no arguments - and returns an object of type ``point`` Since in this case, there is no such function, it returns an error. Some compilers, like *clang*, will try to tell you: .. code-block:: none point.cpp:7:12: warning: empty parentheses interpreted as a function declaration [-Wvexing-parse] point p(); ^~ point.cpp:7:12: note: replace parentheses with an initializer to declare a variable point p(); ^~ .. cpp:: 11 C++ resolves this ambiguity in C++11 using the *uniform initializer syntax*. You can use curly braces: ``{}`` instead of parentheses to initialize objects. Braces are an extension of the initializer list syntax for containers and can be used even for default constructed objects. .. code-block:: cpp string s{}; vector v{}; point p{}; string s{"hello, world!"}; vector v{1,2,3,4,5}; While the above works every time, omitting the braces entirely when not needed is preferred: .. code-block:: cpp string s; vector v; point p; Initializer syntax works within constructors as well. .. code-block:: cpp vector redundant { point {2,3}, point {3,5}, point {8,13}, point {21,34} }; // The compiler can deduce the type in the container, // so we don't have to repeat the type every time vector points { {2,3}, {3,5}, {8,13}, {21,34} }; Recall that for containers, there is a difference between ``vector(5)`` and ``vector{5}``. What's the difference? .. tb-reveal:: :name: reveal-whats-the-difference The first version creates a vector of size 5 with no initialized values. The second version creates a vector of size 1 with a single value equal to 5. Overloaded constructors ----------------------- The same guidelines that apply to writing good functions apply to writing good overloaded constructors. A good class is built around good functions. Just as with regular functions, avoid confusing parameter lists. Consider the following: .. code-block:: cpp date (int, int, int); It's seems likely that the three parameters represent the year, month, and day, but without reading the code, there is no way to know what order. .. code-block:: cpp // is this correct? date d = {1776, 7, 4}; // or this? date d = {4, 1776, 7}; Even if we read the code and learn the order, it is still probable that we will forget the order and transpose a month and day at some point. Instead of resigning ourselves to hoping we remember or having debugging problems at runtime, simply defining appropriate types improves clarity and utility: .. code-block:: cpp class year { int y; public: year() = default; year(int value) : y {value} {} int year() { return y;} }; enum class month { jan=1, feb, mar, apr, may, jun, jul, aug, sep, oct, nov, dec }; // now a new date can be constructed like: date d = {year{1776}, month::jul, 4}; This version is easier for programmers to remember and any errors are compile errors instead of runtime errors. Telescoping constructors ------------------------ The original ``date`` class suffered from a common design problem: too many parameters of the same type. A closely related problem is how to provide flexibility when constructing new objects. A common solution is to provide constructors with different numbers of arguments: .. code-block:: cpp struct date { date(); // make a default date date(year y); // use a default month and day date(year y, month m); // use a default day date(year y, month m, int day); // specify the entire date }; What about the possibility of specifying ``month`` and ``day``? How many different constructors should be allowed? The number of permutations get unmaintainable even for a relatively few number of parameters. This is called a *telescoping constructor*, and is generally considered an :term:`anti-pattern`. That is, there are better solutions to this problem. The easiest solution in C++ is to use default values for function parameters. This works best when the default values are different types and there is no need to allow every possible combination of parameters. .. code-block:: cpp struct date { date(year y = date::current_year(), month m = date::current_month(), int day = date::current_day()); }; This solution is still limited by the fact that defaults are still evaluated left to right. A date declaration of the form .. code-block:: cpp date d {15}; won't create a date for the 15th day of the current month and year. In addition, the solution does not work well when all (or most) of the parameters are the same type. Consider this example: .. code-block:: cpp class nutrition_facts { private: // variables in need of initialization to make valid object const double serving_size_; // mL const int servings_; // per container const double calories_; // Kcal const double fat_; // g const double sodium_; // mg const double carbs_; // g public: // How about this as a solution? nutrition_facts(double, int, double, double, double, double); }; Is the proper order ``calories``, ``fat``, ``carbs``, or ``fat``, ``calories``, ``carbs``, or something else? Even if we give these parameters meaningful names, there is no runtime enforcement. It's easy to make a mistake when too many parameters are the same type. When confronted with many optional parameters, a builder is an effective alternative. Basic ideas: - Use constructor parameters to accept mandatory parameters. - Use a helper class (builder) to default initialize optional parameters. - A ``builder::build()`` function creates a nutrition_facts object from a builder. - The builder makes the class it helps a friend. This is used only avoid creating builder accessor functions. - A conversion constructor is used to copy builder state into the enclosing class. .. code-block:: cpp :name: nutrition_facts.h :caption: A nutrition facts class #include class nutrition_facts { private: // variables in need of initialization to make valid object const double serving_size_; // mL const int servings_; // per container const double calories_; // Kcal const double fat_; // g const double sodium_; // mg const double carbs_; // g public: // Only one simple constructor for mandatory parameters. // The remaining members are handled by builder nutrition_facts( const double serving_size, const int servings) : serving_size_{serving_size}, servings_{servings}, calories_{0}, fat_{0}, sodium_{0}, carbs_{0} {} // use this class to construct nutrition_facts class builder { private: friend nutrition_facts; double serving_size_ = 15; // mL int servings_ = 10; // per container double calories_ = 0; // Kcal double fat_ = 0; // g double sodium_ = 0; // mg double carbs_ = 0; // g public: builder() = default; // create a nutrition_facts object from a builder nutrition_facts build() { return nutrition_facts (*this); } builder& serving_size(const double size) { serving_size_ = size; return *this; } builder& servings(const int s) { servings_ = s; return *this; } builder& calories(const double c) { calories_ = c; return *this; } builder& fat(const double f) { fat_ = f; return *this; } builder& sodium(const double s) { sodium_ = s; return *this; } builder& carbohydrates(const double c) { carbs_ = c; return *this; } }; explicit nutrition_facts(const builder& builder) : serving_size_{builder.serving_size_}, servings_{builder.servings_}, calories_{builder.calories_}, fat_{builder.fat_}, sodium_{builder.sodium_}, carbs_{builder.carbs_} {} double serving_size() const { return serving_size_; } int servings() const { return servings_; } double calories() const { return calories_; } double fat() const { return fat_; } double sodium() const { return sodium_; } double carbohydrates() const { return carbs_; } }; std::ostream& operator<<(std::ostream& os, const nutrition_facts& rhs) { return os << "Serving size: " << rhs.serving_size() << "\tServings: " << rhs.servings() << "\n\tCal: " << rhs.calories() << "\n\tFat: " << rhs.fat() << "\n\tSodium: " << rhs.sodium() << "\n\tCarbs: " << rhs.carbohydrates(); } When complete, the classes can be used like this: .. tb-code:: cpp :run-before: nutrition_facts.h #include int main() { // make facts without any optional parts auto cake = nutrition_facts{75, 8}; // create a builder nutrition_facts::builder b; // change the state b.serving_size(28.4).servings(1); b.fat(10).sodium(2).calories(150).carbohydrates(15); // create a set of nutrition facts using the builder auto chips = b.build(); // create nutrition facts without creating a (named) // temporary builder object auto soda = nutrition_facts::builder() .serving_size(368).servings(1) .carbohydrates(40).calories(150).sodium(15).build(); std::cout << "Cake:\t" << cake << '\n'; std::cout << "Chips:\t" << chips << '\n'; std::cout << "Soda:\t" << soda << '\n'; return 0; } While not the most idiomatic C++ solution, it is something we can create and use with only the knowledge of classes we have so far. ----- .. admonition:: More to Explore - :wiki:`Most vexing parse (wikipedia) ` - Item #6 "Most Vexing Parse" from 'Effective STL' by Scott Meyers (Addison-Wesley Professional). Copyright 2001 Scott Meyers, 978-0-201-74962-5. - Builder design pattern: - `Builder Design Pattern `__ on oodesign.com - :wiki:`Builder Design Pattern ` on Wikipedia - `Example telescoping constructor `__ - Effective Java, by Joshua Bloch. Item #2: Consider a builder when faced with many constructor parameters