ghostbody / 15-cpp-learning

15 c++ learning repository of SDCS SYSU
6 stars 3 forks source link

C++11 Learning Summary #14

Open DaddyTrap opened 8 years ago

DaddyTrap commented 8 years ago

Chapter 1: auto

auto is not a new key word in C++, but in C++11 it has a new feature (and the feature in C++98 was disabled). Using auto, for example, auto i = 1, means we let the compiler judge the type of i. auto will be treated as int here. And you can write auto a = getX(), then you can easily get the X without thinking more about its type. You can remember your code, but maybe you will forget others' code. So, it is convenient.

Somebody thinks it makes no sense to add this feature, but it is really a delicious sugar!(Syntactic sugar I mean) For instance, please see the following code.

#include <iostream>
#include <vector>
#define CPP98
using std::cout;
using std::endl;
int main() {
  int arr[10] = {};
  for (int i = 0; i < 10; ++i) {
    arr[i] = i;
  }
  std::vector<int> v(arr, arr + 10);
#ifdef CPP98
  for (std::vector::iterator it = v.begin(); it != v.end(); ++it) {
    cout << *it << " ";
  }
  cout << endl;
#endif

#ifdef CPP11
  for (auto it = v.begin(); it != v.end(); ++it) {
    cout << *it << " ";
  }
  cout << endl;
#endif

  return 0;
}

At the first time I compile it, I got some compile errors, such as

main2.cpp:13:13: error: ‘template<class _Tp, class _Alloc> class std::vector’ used without template parameters
   for (std::vector::iterator it = v.begin(); it != v.end(); ++it) {
             ^
main2.cpp:13:30: error: expected ‘;’ before ‘it’
   for (std::vector::iterator it = v.begin(); it != v.end(); ++it) {
                              ^
main2.cpp:13:46: error: ‘it’ was not declared in this scope
   for (std::vector::iterator it = v.begin(); it != v.end(); ++it) {
                                              ^

Finally I found that I didn't give the template to vector. But if I use auto, it won't annoy me. And when we use some libraries, for example, cocos2d-x. It use factory mode to produce instance, so auto is used generally.


The final and correct code:

#include <iostream>
#include <vector>
#define CPP11
using std::cout;
using std::endl;
int main() {
  int arr[10] = {};
  for (int i = 0; i < 10; ++i) {
    arr[i] = i;
  }
  std::vector<int> v(arr, arr + 10);
#ifdef CPP98
  for (std::vector<int>::iterator it = v.begin(); it != v.end(); ++it) {
    cout << *it << " ";
  }
  cout << endl;
#endif

#ifdef CPP11
  for (auto it = v.begin(); it != v.end(); ++it) {
    cout << *it << " ";
  }
  cout << endl;
#endif

  return 0;
}
DaddyTrap commented 8 years ago

Chapter 2: Lambda expression

Lambda expression is a really new part in C++11. It is said that this feature is inspired by Functional Programming. This expression support Anonymous function(匿名函数). Let's see the expression.

capture -> return_type { function_body } // from wiki

With this, we can simply declare a function and use it.

// declare
auto func = [a](int b) -> int {
  return a + b;
};
// use it
cout << func(10) << endl;

capture means this function can catch some variables including global variables and use them in the function body before it. capture有翻译为“捕获”,指可以捕获上文的一些变量在函数体中使用,包括全局变量。 parameters are the parameters, the same as what we usually use. parameter 参数,就是平常用的参数。 return_type is the type that the function return, same 返回类型,一样的 function_body, same 函数体,一样……

Then let's concentrate on capture.....

#include <iostream>
using std::cout;
using std::endl;
int global = 3;
int main() {
  int temp = 1;
  auto func1 = [global, temp]() -> int {
    return global + temp;
  };
  cout << func1() << endl;
  return 0;
}

output:

4

This is a simple example. The function "catch" global and temp and return their sum.

When we try catching the variable after the lambda expression, it can't find it.

#include <iostream>
using std::cout;
using std::endl;
int global = 3;
int main() {
  auto func1 = [global, temp]() -> int {
    return global + temp;
  };
  int temp = 1;  // put after the lambda expression
  cout << func1() << endl;
  return 0;
}
lambda.cpp:6:25: error: ‘temp’ was not declared in this scope
   auto func1 = [global, temp]() -> int {
                         ^
lambda.cpp: In lambda function:
lambda.cpp:7:21: error: ‘temp’ is not captured
     return global + temp;
                     ^

If you want to catch all the variable by value, you can use =.

#include <iostream>
using std::cout;
using std::endl;
int global = 3;
int main() {
  int temp = 1;
  auto func1 = [=]() -> int {  // change the variable names to "="
    return global + temp;
  };
  cout << func1() << endl;
  return 0;
}

output:

4

If you want to catch them by reference, use &.

#include <iostream>
using std::cout;
using std::endl;
int global = 3;
int main() {
  int temp = 1;
  auto func1 = [&]() -> int {
    temp = 2;
    return global + temp;
  };
  cout << func1() << endl;
  cout << "temp: " << temp << endl;
  return 0;
}

output:

5
temp: 2

Now the basic knowledge is above.


DaddyTrap commented 8 years ago

Chapter 3: R-value references and move constructors

R-value reference

First we have to understand what is L-value(左值) and R-value(右值). Generally, L-value is the value on the left of an assignment character "=", R-value is the value on the right. But this statement is not very reasonable.

L-values have storage addresses that are programmatically accessible to the running program (e.g., via some address-of operator like "&" in C/C++) R-values can be l-values (see below) or non-l-values—a term only used to distinguish from l-values

Simply speaking, when "&" can be used to get a value's address, then this value is a l-value. R-value can be a l-value or not. A temporary value is exactly a r-value. For example, a return value from a function.


The explanation about r-value is above. Next, let's go to the main topic, r-value reference. When we pass values to a function, for example

int sum(int a, int b) {
  return a + b;
}
int main() {
  cout << sum(3, 4) << endl;
  return 0;
}

3 and 4 will be copied. Though copying an int is not very difficult, but when copying an object, it is an awesome job.


Why not use the allocated resource? Doing so, we can reduce the times to allocate new memory. Then we come to move semantic.

Move semantic 移动语义

Move means the L-value "steal" the R-value. Well, even if it was not stolen, its value will be lost because R-value is a temporary value. So, using move semantic can prevent its lost and give it to L-value. In this way we construct the value only once.

Let's make a class MyString to use it.

#include <iostream>
#include <cstdio>
#include <string>
#include <cstring>
#include <vector>
using std::cout;
using std::endl;

struct MyString {
  char *s;
  size_t len;

  MyString() {
    s = NULL;
    len = 0;
  }

  ~MyString() {
    if (s) delete[] s;
  }

  MyString(const MyString &f) {
    cout << "Copy constructor" << endl;
    len = f.len;
    s = f.s;
    cout << "new space was allocated" << endl;
    s = new char[len + 1];
    memcpy(s, f.s, len);
    s[len] = '\0';
  }

  MyString(const char *s) {
    cout << "Const char* constructor" << endl;
    len = strlen(s);
    this->s = new char[len + 1];
    memcpy(this->s, s, len);
    this->s[len] = '\0';
  }

  MyString &operator=(const MyString &f) {
    cout << "Copy Assignment" << endl;
    if (s) delete[] s;
    len = f.len;
    s = f.s;
    cout << "new space was allocated" << endl;
    s = new char[len + 1];
    memcpy(s, f.s, len);
    s[len] = '\0';
    return *this;
  }
};

int main() {
  MyString s2;
  s2 = MyString("Test");
  std::vector<MyString> v;
  v.push_back(MyString("Test"));
  return 0;
}

With this code, we get

Const char* constructor
Copy Assignment
new space was allocated
Const char* constructor
Copy constructor
new space was allocated

Using move semantic

#include <iostream>
#include <cstdio>
#include <string>
#include <cstring>
#include <vector>
using std::cout;
using std::endl;

struct MyString {
  char *s;
  size_t len;

  MyString() {
    s = NULL;
    len = 0;
  }

  MyString(MyString &&f) {  // here different
    cout << "Move constructor" << endl;
    len = f.len;
    cout << "nothing new was allocated" << endl;
    s = f.s;
    f.s = NULL;
    f.s = 0;
  }

  ~MyString() {
    if (s) delete[] s;
  }

  MyString(const char *s) {
    cout << "Const char* constructor" << endl;
    len = strlen(s);
    this->s = new char[len + 1];
    memcpy(this->s, s, len);
    this->s[len] = '\0';
  }

  MyString &operator=(MyString &&f) {  // here different
    cout << "Move Assignment" << endl;
    if (s) delete[] s;
    len = f.len;
    cout << "nothing new was allocated" << endl;
    s = f.s;
    f.s = NULL;
    f.s = 0;
    return *this;
  }
};

int main() {
  MyString s2;
  s2 = MyString("Test");
  std::vector<MyString> v;
  v.push_back(MyString("Test"));
  return 0;
}

Then we get

Const char* constructor
Move Assignment
nothing new was allocated
Const char* constructor
Move constructor
nothing new was allocated

When moved, no new memory will be allocated. So our program can be more efficient.

In addition, usually we should declare both copy constructor and move constructor. Because when we have to copy something, we can't just move it.

DaddyTrap commented 8 years ago

Chapter 4: unique_ptr

unique_ptr means unique pointer. That is, the pointer is the unique one to manage an object and it is responsible to delete the object when the pointer itself is deleted. Yes, it deletes the object in its destructor. It is useful because we can get no memory errors when using it properly. Next, let's see how we can use it.

#include <iostream>
#include <memory>

using std::unique_ptr;
int main() {
  unique_ptr<int> p(new int(10));
  std::cout << "\n";
  return 0;
}

No delete!

==11764== Memcheck, a memory error detector
==11764== Copyright (C) 2002-2013, and GNU GPL'd, by Julian Seward et al.
==11764== Using Valgrind-3.10.1 and LibVEX; rerun with -h for copyright info
==11764== Command: ./a.out
==11764== 

==11764== 
==11764== HEAP SUMMARY:
==11764==     in use at exit: 72,704 bytes in 1 blocks
==11764==   total heap usage: 2 allocs, 1 frees, 72,708 bytes allocated
==11764== 
==11764== LEAK SUMMARY:
==11764==    definitely lost: 0 bytes in 0 blocks
==11764==    indirectly lost: 0 bytes in 0 blocks
==11764==      possibly lost: 0 bytes in 0 blocks
==11764==    still reachable: 72,704 bytes in 1 blocks
==11764==         suppressed: 0 bytes in 0 blocks
==11764== Rerun with --leak-check=full to see details of leaked memory
==11764== 
==11764== For counts of detected and suppressed errors, rerun with: -v
==11764== ERROR SUMMARY: 0 errors from 0 contexts (suppressed: 0 from 0)

BUT no error!

And you can use it to manage array!

#include <iostream>
#include <memory>

using std::unique_ptr;
int main() {
  unique_ptr<int> p(new int[3]{1, 2, 3});
  std::cout << "\n";
  p.reset();
  return 0;
}

No delete!

==11778== Memcheck, a memory error detector
==11778== Copyright (C) 2002-2013, and GNU GPL'd, by Julian Seward et al.
==11778== Using Valgrind-3.10.1 and LibVEX; rerun with -h for copyright info
==11778== Command: ./a.out
==11778== 

==11778== Mismatched free() / delete / delete []
==11778==    at 0x4C2C2BC: operator delete(void*) (in /usr/lib/valgrind/vgpreload_memcheck-amd64-linux.so)
==11778==    by 0x400D03: std::default_delete<int>::operator()(int*) const (in /home/daddytrap/Homework/test/a.out)
==11778==    by 0x400C69: std::unique_ptr<int, std::default_delete<int> >::reset(int*) (in /home/daddytrap/Homework/test/a.out)
==11778==    by 0x40096B: main (in /home/daddytrap/Homework/test/a.out)
==11778==  Address 0x5a37c80 is 0 bytes inside a block of size 12 alloc'd
==11778==    at 0x4C2B800: operator new[](unsigned long) (in /usr/lib/valgrind/vgpreload_memcheck-amd64-linux.so)
==11778==    by 0x40091F: main (in /home/daddytrap/Homework/test/a.out)
==11778== 
==11778== 
==11778== HEAP SUMMARY:
==11778==     in use at exit: 72,704 bytes in 1 blocks
==11778==   total heap usage: 2 allocs, 1 frees, 72,716 bytes allocated
==11778== 
==11778== LEAK SUMMARY:
==11778==    definitely lost: 0 bytes in 0 blocks
==11778==    indirectly lost: 0 bytes in 0 blocks
==11778==      possibly lost: 0 bytes in 0 blocks
==11778==    still reachable: 72,704 bytes in 1 blocks
==11778==         suppressed: 0 bytes in 0 blocks
==11778== Rerun with --leak-check=full to see details of leaked memory
==11778== 
==11778== For counts of detected and suppressed errors, rerun with: -v
==11778== ERROR SUMMARY: 1 errors from 1 contexts (suppressed: 0 from 0)

No err....... What the.......?????

Yep, errors occur. Search unique_ptr in cplusplus

non-specialized
template <class T, class D = default_delete> class unique_ptr; array specialization
template <class T, class D> class unique_ptr<T[],D>;

Now I see. When I want to use it to manage array, I should use unique_ptr<T[]> If not, when the pointer want to delete the object, it uses delete but not delete[], and you can use operator[] to access members in the array. (see detail in cplusplus)

The correct code is following.

#include <iostream>
#include <memory>

using std::unique_ptr;
int main() {
  unique_ptr<int[]> p(new int[3]{1, 2, 3});
  std::cout << "\n";
  p.reset();
  return 0;
}