Xathrya Sabertooth |
Ten C++11 Features You Should Know and Use Posted: 04 Dec 2013 05:52 PM PST This article will be a resume to several articles discussing individual subject. There are lots of new additions to the C++ language and standard library after C++11 standard passed. However, I believe some of these new features should become routing for all C++ developers. Features we are talking about:
auto & decltypeMore: Improved Typed Reference in C++11: auto, decltype, and new function declaration syntax Before C++11 era, keyword auto was used for storage duration specification. In the new standard, C++ define clearly the purpose to be type inference. Keyword auto is a placeholder for a type, telling the compiler it has to deduce the actual type of a variable that is being declared from its initializer. auto I = 42; // I is an int auto J = 42LL; // J is an long long auto P = new foo(); // P is a foo* (pointer to foo) Using auto means less code for writing typename. The very convenience use of auto would be inferring type for iterator: std::map<std::string, std::vector<int>> myMap; for (auto it = begin(map); it != end(map); ++it) { //... } Here we save lot of works by order compiler to deduce the type of it. decltype in other hand is a handy keyword to get type of an expression. Here we can inspect what’s going on this code: short a = 10; long b = 655351334; decltype(a+b) c = 5; std::cout << sizeof(a) << " " << sizeof(b) << " " << sizeof(c) << std::endl; When we execute this code using proper C++11 compiler, we got variable c as a type used for summation of a and b. We know that when a short is summed with a long, the short variable will be typecasted automatically to type sufficient enough to hold the result code. And the decltype will give us it’s type. As said before, decltype is used to get type of an expression, therefore it is valid for use to do this: int function(int a, int b) { return a * b; } int main() { decltype(function(a,b)) c = 10; return 0; } As long as the expression involved is valid. Now, in C++ we have a new function declaration syntax. This syntax leverage the power of both auto and decltype. Note that auto cannot be used as the return type of a function, so we must have a trailing return type. In this case auto does not tell the compiler it has to infer the type, it only instructs it to look for the return type at the end of the function. template <typename T1, typename T2> auto compose(T1 t1, T2 t2) -> decltype (t1 + t2) { return t1+t2; } In above snippet, we compose the return type of function from the type of operator + that sums the values of types T1 and T2. nullptrMore: Nullptr, Strongly typed Enumerations, and Cstdint Since the inception of C++, zero is used as the value of null pointers. This is a direct influence from C language. The system itself has drawbacks due to the implicit conversion to integral types. void function(int a); void function(void* a); function(NULL); Now, which one is being called? On smarter compiler, it will gives error saying “the call is ambiguous”. C++11 library gives solution for this. Keyword nullptr denotes a value of type std::nullptr_t that represents the null pointer literal. Implicit conversions exists from nullptr to null pointer value of any pointer type and any pointer-to-member types, but also to bool (as false). But no implicit conversion to integral types exists. void foo(int* p) {} void bar(std::shared_ptr<int> p) {} int* p1 = NULL; int* p2 = nullptr; if(p1 == p2) { } foo(nullptr); bar(nullptr); bool f = nullptr; int i = nullptr; // error: A native nullptr can only be converted to bool or, using reinterpret_cast, to an integral type Using 0 is still valid for backward compatibility. Range-based for loopsMore: C++ Ranged Based Loop Ever wonder how could we do foreach statement in C++? Joy for us because C++11 now augmented the for statement to support it. Using this foreach paradigm we can iterate over collections. In the new form, it is possible to iterate over C-like arrays, initializer lists, and anything for which the non-member begin() and end() functions are overloaded. std::map<std::string, std::vector<int>> map; std::vector<int> v; v.push_back(1); v.push_back(2); v.push_back(3); map["one"] = v; for(const auto& kvp : map) { std::cout << kvp.first << std::endl; for(auto v : kvp.second) { std::cout << v << std::endl; } } int arr[] = {1,2,3,4,5}; for(int& e : arr) { e = e*e; } The syntax is not different from “normal” for statement, okay it is a little different. The for syntax in this paradigm is: for (type iterateVariable : collection) { // ... } Override and finalIn C++, there isn’t a mandatory mechanism to mark virtual methods as overriden in derived class. The virtual keyword is optional and that makes reading code a bit harder, because we may have to look through the top of the hierarchy to check if the method is virtual. class Base { public: virtual void f(float); }; class Derived : public Base { public: virtual void f(int); };
C++11 provides syntax to solve this problem. class Base { public: virtual void f(float); }; class Derived : public Base { public: virtual void f(int) override; }; Keyword override force the compiler to check the base class(es) to see if there is a virtual function with this exact signature. When we compile this code, it will triggers a compile error because the function supposed to override the base class has different signature. On the other hand if you want to make a method impossible to override any more (down the hierarchy) mark it as final. That can be in the base class, or any derived class. If it’s in a derived class, we can use both the override and final specifiers. class Base { public: virtual void f(float); }; class Derived : public Base { public: virtual void f(int) override final; }; class F : public Derived { public: virtual void f(int) override; } Function declared as ‘final’ cannot be overridden by ‘F::f’. Strongly-typed enumsMore: Nullptr, Strongly typed Enumerations, and Cstdint Traditional enums in C++ have some drawbacks: they export their enumerators in the surrounding scope (which can lead to name collisions, if two different enums in the same have scope define enumerators with the same name), they are implicitly converted to integral types and cannot have a user-specified underlying type. C++11 introduces a new category of enums, called strongly-typed enums. They are specified with the “enum class” keyword which won’t export their enumerators in the surrounding scope and no longer implicitly converted to integral types. Thus we can have a user specified underlying type. enum class Options { None, One, All }; Options o = Options::All; Smart pointersAll the pointers are declared in header <memory> In this article we will only mention smart pointers with reference counting and auto releasing of owned memory that are available:
The library type auto_ptr is now obsolete and should no longer be used. The first example below shows unique_ptr usage. If we want to transfer ownership of an object to another void foo(int* p) { std::cout << *p << std::endl; } std::unique_ptr<int> p1(new int(42)); std::unique_ptr<int> p2 = std::move(p1); // transfer ownership if(p1) foo(p1.get()); (*p2)++; if(p2) foo(p2.get()); The second example shows shared_ptr. Usage is similar, though the semantics are different since ownership is shared. void foo(int* p) { std::cout << *p << std::endl; } void bar(std::shared_ptr<int> p) { ++(*p); } std::shared_ptr<int> p1(new int(42)); std::shared_ptr<int> p2 = p1; foo(p2.get()); bar(p1); foo(p2.get()); We can also make equivalent expression for first declaration as: auto p3 = std::make_shared<int>(42); make_shared<T> is a non-member function and has the advantage of allocating memory for the shared object and the smart pointer with a single allocation, as opposed to the explicit construction of a shared_ptr via the contructor, that requires at least two allocations. In addition to possible overhead, there can be situations where memory leaks can occur because of that. In the next example memory leaks could occur if seed() throws an error. void foo(std::shared_ptr<int> p, int init) { *p = init; } foo(std::shared_ptr<int>(new int(42)), seed()); No such problem exists if using make_shared. The last sample shows usage of auto p = std::make_shared<int>(42); std::weak_ptr<int> wp = p; { auto sp = wp.lock(); std::cout << *sp << std::endl; } p.reset(); if(wp.expired()) std::cout << "expired" << std::endl; LambdasMore: Guide to Lambda Closure in C++11 Lambda is anonymous function. It is powerful feature borrowed from functional programming that in turned enabled other features or powered library. We can use lambda wherever a function object or a functor or a std::function is expected. You can read the expression here. std::vector<int> v; v.push_back(1); v.push_back(2); v.push_back(3); std::for_each(std::begin(v), std::end(v), [](int n) {std::cout << n << std::endl;}); auto is_odd = [](int n) {return n%2==1;}; auto pos = std::find_if(std::begin(v), std::end(v), is_odd); if(pos != std::end(v)) std::cout << *pos << std::endl; A bit trickier are recursive lambdas. Imagine a lambda that represents a Fibonacci function. If you attempt to write it using auto you get compilation error: auto fib = [&fib](int n) {return n < 2 ? 1 : fib(n-1) + fib(n-2);}; The problem is auto means the type of the object is inferred from its initializer, yet the initializer contains a reference to it, therefore needs to know its type. This is a cyclic problem. The key is to break this dependency cycle and explicitly specify the function’s type using std::function<int(int)> lfib = [&lfib](int n) {return n < 2 ? 1 : lfib(n-1) + lfib(n-2);}; non-member begin() and end()Two new addition to standard library, begin() and end(), gives new flexibility. It is promoting uniformity, concistency, and enabling more generic programming which work with all STL containers. These two functions are overloadable, can be extended to work with any type including C-like arrays. Let’s take an example. We want to print first odd element on a C-like array. int arr[] = {1,2,3}; std::for_each(&arr[0], &arr[0]+sizeof(arr)/sizeof(arr[0]), [](int n) {std::cout << n << std::endl;}); auto is_odd = [](int n) {return n%2==1;}; auto begin = &arr[0]; auto end = &arr[0]+sizeof(arr)/sizeof(arr[0]); auto pos = std::find_if(begin, end, is_odd); if(pos != end) std::cout << *pos << std::endl; With non-member int arr[] = {1,2,3}; std::for_each(std::begin(arr), std::end(arr), [](int n) {std::cout << n << std::endl;}); auto is_odd = [](int n) {return n%2==1;}; auto pos = std::find_if(std::begin(arr), std::end(arr), is_odd); if(pos != std::end(arr)) std::cout << *pos << std::endl; This is basically identical code to the template <typename Iterator> void bar(Iterator begin, Iterator end) { std::for_each(begin, end, [](int n) {std::cout << n << std::endl;}); auto is_odd = [](int n) {return n%2==1;}; auto pos = std::find_if(begin, end, is_odd); if(pos != end) std::cout << *pos << std::endl; } template <typename C> void foo(C c) { bar(std::begin(c), std::end(c)); } template <typename T, size_t N> void foo(T(&arr)[N]) { bar(std::begin(arr), std::end(arr)); } int arr[] = {1,2,3}; foo(arr); std::vector<int> v; v.push_back(1); v.push_back(2); v.push_back(3); foo(v); static_assert and type traits
template <typename T, size_t Size> class Vector { static_assert(Size < 3, "Size is too small"); T _points[Size]; }; int main() { Vector<int, 16> a1; Vector<double, 2> a2; return 0; } static_assert becomes more useful when used together with type traits. These are a series of classes that provide information about types at compile time. They are available in the <type_traits> header. There are several categories of classes in this header: helper classes, for creating compile-time constants, type traits classes, to get type information at compile time, and type transformation classes, for getting new types by applying transformation on existing types. In the following example function add is supposed to work only with integral types. template <typename T1, typename T2> auto add(T1 t1, T2 t2) -> decltype(t1 + t2) { return t1 + t2; } However, there are no compiler errors if one writes std::cout << add(1, 3.14) << std::endl; std::cout << add("one", 2) << std::endl; The program actually prints 4.14 and “e”. But if we add some compile-time asserts, both these lines would generate compiler errors. template <typename T1, typename T2> auto add(T1 t1, T2 t2) -> decltype(t1 + t2) { static_assert(std::is_integral<T1>::value, "Type T1 must be integral"); static_assert(std::is_integral<T2>::value, "Type T2 must be integral"); return t1 + t2; } Move semanticsMore: Move Semantics and rvalue references in C++11 C++11 has introduced the concept of rvalue references (specified with &&) to differentiate a reference to an lvalue or an rvalue. An lvalue is an object that has a name, while an rvalue is an object that does not have a name (temporary object). The move semantics allow modifying rvalues (previously considered immutable and indistinguishable from const& types). A C++ class/struct used to have some implicit member functions: default constructor (only if another constructor is not explicitly defined) and copy constructor, a destructor and a copy assignment operator. The copy constructor and the copy assignment operator perform a bit-wise (or shallow) copy, i.e. copying the variables bitwise. That means if you have a class that contains pointers to some objects, they just copy the value of the pointers and not the objects they point to. This might be OK in some cases, but for many cases you actually want a deep-copy, meaning that you want to copy the objects pointers refer to, and not the values of the pointers. In this case you have to explicitly write copy constructor and copy assignment operator to perform a deep-copy. What if the object you initialize or copy from is an rvalue (a temporary). You still have to copy its value, but soon after the rvalue goes away. That means an overhead of operations, including allocations and memory copying that after all, should not be necessary. Enter the move constructor and move assignment operator. These two special functions take a T&& argument, which is an rvalue. Knowing that fact, they can modify the object, such as “stealing” the objects their pointers refer to. For instance, a container implementation (such as a vector or a queue) may have a pointer to an array of elements. When an object is instantiating from a temporary, instead of allocating another array, copying the values from the temporary, and then deleting the memory from the temporary when that is destroyed, we just copy the value of the pointer that refers to the allocated array, thus saving an allocation, copying a sequence of elements, and a later deallocation. The following example shows a dummy buffer implementation. The buffer is identified by a name (just for the sake of showing a point revealed below), has a pointer (wrapper in an template <typename T> class Buffer { std::string _name; size_t _size; std::unique_ptr<T[]> _buffer; public: // default constructor Buffer(): _size(16), _buffer(new T[16]) {} // constructor Buffer(const std::string& name, size_t size): _name(name), _size(size), _buffer(new T[size]) {} // copy constructor Buffer(const Buffer& copy): _name(copy._name), _size(copy._size), _buffer(new T[copy._size]) { T* source = copy._buffer.get(); T* dest = _buffer.get(); std::copy(source, source + copy._size, dest); } // copy assignment operator Buffer& operator=(const Buffer& copy) { if(this != ©) { _name = copy._name; if(_size != copy._size) { _buffer = nullptr; _size = copy._size; _buffer = _size > 0 > new T[_size] : nullptr; } T* source = copy._buffer.get(); T* dest = _buffer.get(); std::copy(source, source + copy._size, dest); } return *this; } // move constructor Buffer(Buffer&& temp): _name(std::move(temp._name)), _size(temp._size), _buffer(std::move(temp._buffer)) { temp._buffer = nullptr; temp._size = 0; } // move assignment operator Buffer& operator=(Buffer&& temp) { assert(this != &temp); // assert if this is not a temporary _buffer = nullptr; _size = temp._size; _buffer = std::move(temp._buffer); _name = std::move(temp._name); temp._buffer = nullptr; temp._size = 0; return *this; } }; template <typename T> Buffer<T> getBuffer(const std::string& name) { Buffer<T> b(name, 128); return b; } int main() { Buffer<int> b1; Buffer<int> b2("buf2", 64); Buffer<int> b3 = b2; Buffer<int> b4 = getBuffer<int>("buf4"); b1 = getBuffer<int>("buf5"); return 0; } The default copy constructor and copy assignment operator should look familiar. What’s new to C++11 is the move constructor and move assignment operator, implemented in the spirit of the aforementioned move semantics. If you run this code you’ll see that when b4 is constructed, the move constructor is called. Also, when b1 is assigned a value, the move assignment operator is called. The reason is the value returned by You probably noticed the use of std::move in the move constructor, when initializing the name variable and the pointer to the buffer. The name is actually a string, and UPDATE: Though the purpose of this example was to show how move constructor and move assignment operator should be implemented, the exact details of an implementation may vary. An alternative implementation was provided by Member 7805758 in the comments. To be easier to see it I will show it here: template <typename T> class Buffer { std::string _name; size_t _size; std::unique_ptr<T[]> _buffer; public: // constructor Buffer(const std::string& name = "", size_t size = 16): _name(name), _size(size), _buffer(size? new T[size] : nullptr) {} // copy constructor Buffer(const Buffer& copy): _name(copy._name), _size(copy._size), _buffer(copy._size? new T[copy._size] : nullptr) { T* source = copy._buffer.get(); T* dest = _buffer.get(); std::copy(source, source + copy._size, dest); } // copy assignment operator Buffer& operator=(Buffer copy) { swap(*this, copy); return *this; } // move constructor Buffer(Buffer&& temp):Buffer() { swap(*this, temp); } friend void swap(Buffer& first, Buffer& second) noexcept { using std::swap; swap(first._name , second._name); swap(first._size , second._size); swap(first._buffer, second._buffer); } }; |
You are subscribed to email updates from Xathrya Sabertooth To stop receiving these emails, you may unsubscribe now. | Email delivery powered by Google |
Google Inc., 20 West Kinzie, Chicago IL USA 60610 |
Tidak ada komentar:
Posting Komentar