Thursday, December 22, 2016

C++ tips, 2016 Week 50 (12-Dec - 18-Dec-2016)

This is part of my weekly C++ posts based on the daily C++ tips I make at my work. I strongly recommend this practice. If you dont have it in your company start it. 
List of all weekly posts can be found here.


1. Inherited constructors

Inherited constructors are used to reduce the boilerplate code in the case when the delivered class does not have new data members. In the case of this base class:

class Base 
{ 
public:
Base(int x, int y) : x_(x), y_(y) {} 
public: 
int x_; 
int y_; 
};

We can deliver from it:

class Derived : public Base 
{ 
using Base::Base; 
};


Yes - one could call the base class constructor in the member initializer list:
Derived(int x, int y) : Base(x, y) {}
And it is usually OK if you have one constructor but if you have many constructors or constructors with ellipses (unfortunately only implemented in Clang as of today) it gets inefficient.

The main shoot-yourself-in-the-foot opportunity here is to add a data member to the derived class and forget that it must be initialized.

2. std::clamp

Coming in C++17 std::clamp is a function that given a range [lo, hi] and a value v returns v if it is in the range otherwise returns lo or hi. So instead of writing this:

int value;
          if (value < 0)
value = 0; 
if (value > max_value) 
value = max_value;

we can just write:
value = std::clamp(value, 0, max_value);
The the type in this case should meet requirements for LessThanComparable or you should provide a Compare operation. And since this is C++ it is your responsibility to make sure that lo "<" hi.

Because std::clamp requires only LessThanComparable this also works with random access iterators. This is perfectly valid code:

std::vector<int> vect{ -1, 5, 1, 20, 3, 0, -7, 9, 3, 5 }; 

auto itLo = std::begin(vect); 
auto itHi = std::end(vect); 
std::advance(itLo, 2); 
std::advance(itHi, -2); 

// we are comparing iterators - possitions in the vector 
auto a = std::clamp(std::find(std::begin(vect), std::end(vect), 3), itLo, itHi); 
std::cout << *a << '\n';
auto b = std::clamp(std::find(std::begin(vect), std::end(vect), -1), itLo, itHi); 
std::cout << *b << '\n';  
auto c = std::clamp(std::find(std::begin(vect), std::end(vect), 9001), itLo, itHi); 
std::cout << *b << '\n'; // over nine thousand is not a member of vect so find returns vect.end() - still valid

3. Moved-from state

The questions "In what state is a moved-from object?" or "What can I do with moved-from object?" often arises.

The standard does not specify in what state you should leave your moved-from objects (aside from being safely destructible) it only says that about the objects of types defined in the C++ standard library:
17.6.5.15 Moved-from state of library types                                    [lib.types.movedfrom]
Objects of types defined in the C++ standard library may be moved from (12.8). Move operations may be explicitly specified or implicitly generated. Unless otherwise specified, such moved-from objects shall be placed in a valid but unspecified state.
Valid but unspecified state. This means that you can not make any assumptions about the state they are in but you can use them with any operations that do not need any preconditions fulfilled. For example (taken from this very informative lecture about move semantics by Howard E. Hinnant):

std::vector<int> movedFrom{ 1, 2, 3, 4, 5, 6, 7, 8, 9}; 
std::vector<int> vect = std::move(movedFrom);
// does not care of the state of movedFrom as long as it is valid 
movedFrom.clear(); 
// does not require preconditions too but the result is unspecified 
movedFrom.size(); 
// possibly an undefined behavior - has precondition, requires non-empty vector 
movedFrom.pop_back();

For more on the topic read C++ and Zombies: a moving question by Jens Weller

4.  Cache oblivious algorithms

or cache-transcendent algorithms generally speaking are algorithms designed to take advantage from (or not taking a performance hit by ignoring) the multiple levels of cache in current hardware architectures. Usually they split recursively the problem into smaller sub-problems until they can fit into the L1 cache and also sometimes keep L2 cache properly fed.

Once you start measuring performance you soon realize that the big O notation ignores the hardware architecture and often algorithms and structures with better big O lead to worse measured performance. Most popular example being comparison of std::list and std::vectorDay 1 Keynote - Bjarne Stroustrup: C++11 Style (from 44:30).

It is advisable that you study several such algorithms.

5. boost::lexical_cast

boost::lexical_cast is a handy tool for converting between types for example strings to ints, int to strings, etc. Extremely simple use case:
std::string str = boost::lexical_cast<std::string>(10);
int value = boost::lexical_cast<int>("10");
But you should realize that there is nothing magical about it. When we debug it (and I strongly advice for debugging Boost or any other complex libraries from time to time mainly to overcome the fear of debugging sophisticated source code) we see that it eventually boils down to (copied directly from Boost implementation):
static inline bool try_convert(const Source& arg, Target& result) {
    i_interpreter_type i_interpreter;

    // Disabling ADL, by directly specifying operators.
    if (!(i_interpreter.operator <<(arg)))
        return false;

    o_interpreter_type out(i_interpreter.cbegin(), i_interpreter.cend());

    // Disabling ADL, by directly specifying operators.
    if (!(out.operator >> (result)))
        return false;

    return true;
}
Where i_ and o_interpreter_type are basically (revealed after little digging and looking up the documentation) STL streams. Which means that if you write operator<< and operator>> for your own types you can use boost::lexical_cast to cast between them. From Boost documentation:
  • Source must be OutputStreamable, meaning that an operator<< is defined that takes a std::ostream or std::wostream object on the left hand side and an instance of the argument type on the right.
  • Target must be InputStreamable, meaning that an operator>> is defined that takes a std::istream or std::wistream object on the left hand side and an instance of the result type on the right.

No comments:

Post a Comment