The best way to pass function parameters and the rules of modern C++

The best way to pass function parameters and the rules of modern C++

In C++, how to best pass function parameters and how to handle special member functions of classes have always been important topics for optimizing performance and code quality. Below I will explain these concepts in detail.

Use move semantics to implement the Swap function

An example of how Move Semantics can improve performance is by implementing a swap function template that swaps two objects. The implementation without using move semantics is as follows:

template <typename T>
void swapCopy(T & amp; a, T & amp; b) {<!-- -->
    T temp {<!-- --> a };
    a = b;
    b = temp;
}

This implementation can impact performance, especially when copying type T is expensive. Using move semantics, implementations can avoid all copies:

template <typename T>
void swapMove(T & amp; a, T & amp; b) {<!-- -->
    T temp {<!-- --> std::move(a) };
    a = std::move(b);
    b = std::move(temp);
}

This is how std::swap() is implemented in the standard library.

Use std::move() in return statements

If a return statement is of the form return object; and object is a local variable, function parameter, or temporary value, the compiler treats it as an rvalue expression and Trigger return value optimization (RVO). Additionally, named return value optimization (NRVO) may also occur if object is a local variable. RVO and NRVO are both forms of copy elision, making returning objects from functions very efficient.

What about using std::move() to return an object? Whether you write return object; or return std::move(object);, in both cases the compiler will treat it as an rvalue expression. However, with std::move(), the compiler can no longer apply RVO or NRVO, which can have a significant impact on performance!

So, remember the following rule: When returning a local variable or parameter from a function, just write return object;, do not use std::move().

The best way to pass parameters

Until now, it was recommended to use const reference parameters for function parameters of non-primitive types to avoid unnecessary expensive copies. However, with the introduction of rvalues, the situation changes slightly. Imagine a function that copies its arguments no matter what. Now, you might want to add an overload to avoid any copying in the case of rvalues. Here’s an example:

class DataHolder {<!-- -->
public:
    void setData(const std::vector<int> & amp; data) {<!-- -->
        m_data = data;
    }
    void setData(std::vector<int> & amp; & amp; data) {<!-- -->
        m_data = std::move(data);
    }

private:
    std::vector<int> m_data;
};

However, there is a better way, which involves using a single method that is passed by value. For parameters that the function will copy, using pass-by-value semantics is the best choice. If an lvalue is passed in, it is copied exactly once. If an rvalue is passed in, no copy will be made.

Rule of Zero

In modern C++, the so-called Rule of Zero should be followed. This rule states that you should design your classes so that they do not require any special member functions. How to do this? Basically, you should avoid using any old-fashioned dynamic allocation of memory. Instead, modern constructs like standard library containers should be used. For example, use vector> instead of the SpreadsheetCell** data member in the Spreadsheet class. vector handles memory automatically, so no special member functions are required.

The Rule of Zero is recommended in modern C++, while the Rule of Five should be limited to custom Resource Acquisition Is Initialization (RAII) classes. The RAII class takes ownership of the resource and handles its release when appropriate. This is a design technique used, for example, by vector and unique_ptr, and is discussed further in subsequent chapters.

Static methods and const methods are two important concepts in C++, and they each play an important role in different situations.

Static Methods

Static methods are those methods that exist independently of an instance of a class. Similar to static data members, static methods apply to the entire class rather than to each object. When implementing static methods, you need to pay attention to the following points:

  • Static methods are not called on a specific object, so they do not have a this pointer and cannot access non-static members of the class.
  • Static methods can access private and protected static members of a class, as well as private and protected non-static members on objects of the same type, provided that those objects are visible to the static method (for example, by passing a reference to the object as an argument or pointer).
  • Static methods can be called like regular member functions from any method inside a class. Outside the class, you need to use the scope resolution operator (::) with the class name to call the static method.

For example:

Foo::bar();

const Methods

The const method is a method that guarantees that no data members will be modified. If you have a const object that references a const or a pointer to const, the compiler will not allow you to call unless it is a const method. By using the const keyword when declaring a method, you can guarantee that the method will not modify any data members.

For example:

double SpreadsheetCell::getValue() const {<!-- -->
    return m_value;
}

std::string SpreadsheetCell::getString() const {<!-- -->
    return doubleToString(m_value);
}
  • Inside a const method, all data members are treated as const, so if you try to modify the data members, the compiler will raise an error.
  • Static methods cannot be declared as const because this is redundant. Static methods do not have an instance of the class, so they cannot change internal values.
  • const and non-const methods can be called on non-const objects. However, const methods can only be called on const objects.

Mutable Data Members

Sometimes you might write a method that is logically const but happens to change one of the object’s data members. This modification has no effect on user-visible data, but is technically a change, so the compiler won’t let you declare the method as const. In this case, you can use the mutable keyword to declare data members that can be modified even within a const method.

For example:

class SpreadsheetCell {<!-- -->
    // ...
private:
    double m_value {<!-- --> 0 };
    mutable size_t m_numAccesses {<!-- --> 0 };
    // ...
};

double SpreadsheetCell::getValue() const {<!-- -->
    m_numAccesses + + ;
    return m_value;
}

std::string SpreadsheetCell::getString() const {<!-- -->
    m_numAccesses + + ;
    return doubleToString(m_value);
}

In this example, getValue() and getString() can modify m_numAccesses even though they are marked as const >, because it is declared mutable. This allows a method to modify certain data members while maintaining its const nature.

Reference: Professional C++ 5Th Edition

Public account: coding diary