Cpp_值类型_引用折叠

static_cast

int a不能直接用int && e接收。

1
2
3
4
5
6
int main()
{
int a = 12;
int && e = a; // error
e = 13;
}

int a强转为int&&时,可以用int && e接收,修改e,会连带修改a。

1
2
3
4
5
6
int main()
{
int a = 12;
int && e = static_cast<int&&>(a);
e = 13; // e = 13; a = 13
}

int a强转为int时,可以用int && e接收,修改e,不会连带修改a。

为什么?
强转为值类型时,相当于拷贝了一份临时对象(另创空间)。

1
2
3
4
5
6
int main()
{
int a = 12;
int && e = static_cast<int>(a);
e = 13; // e = 13; a = 12
}

移动构造(拷贝右值构造)

  1. 参数不能加const,因为语义上,是要把other对象的资源移动过来,是需要改动other的。
  2. 经验上,要把移动构造函数声明为noexcept,因为涉及到资源的移动的动作过程中即使出现了异常也无法处理、修复。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
class Test
{
public:
Test(int val) : _val { new int(val) }
{}
// 以非右值对象拷贝构造
Test(Test const & other)
{
_val = new int(*other._val);
}
// 以右值对象移动构造
Test(Test && other) noexcept
{
_val = other._val;
other._val = nullptr;
}
~Test()
{
if(_val)
{
delete _val;
_val = nullptr;
}
}
void set_value(int v)
{
if(_val)
{
*_val = v;
}
}
private:
int * _val;
};

测试移动构造:以下代码未能走到移动构造函数的断点——编译器优化了

编译器优化了:Test test{ get_test() }。没走移动构造,而是直接把Test test{5}在main函数中的test上构造了。
这个现象叫做:具名的返回值优化。

1
2
3
4
5
6
7
8
9
Test get_test(void)
{
Test test{5};
return test;
}
int main()
{
Test test{ get_test() };
}

如果不优化的话,会是以下的情况:一共创建了3个对象,有2个是中间无用的临时对象

1
2
3
4
5
6
7
8
9
Test get_test(void)
{
Test test{ 5 }; // create a new test object
return test; // generated a temporary test object
}
int main()
{
Test test{ get_test() };// create a new object by calling copy-constructor
}

为了能看到移动构造函数的断点:不通过函数返回的临时值,通过自己手动创建一个test,来强转为右值引用,从而移动构造新的test。

1
2
3
4
int main()
{
Test test2{ static_cast<Test&&>(test) };
}

赋值重载

以非右值对象赋值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
Test & operator=(Test const& other)
{
if(&other == this) return *this;
if(_val != nullptr)
{
delete _val;
_val = nullptr;
}
if(other._val != nullptr)
{
_val = new int { *other._val };
}
return *this;
}

以右值对象赋值

1
2
3
4
5
6
7
8
9
10
11
Test & operator=(Test && other)
{
if(&other == this) return *this;
if(_val != nullptr)
{
delete _val;
_val = nullptr;
}
_val = other._val;
return *this;
}

static_cast三种值类型的行为

没有变量接收时的行为:

1
2
3
4
5
6
7
8
9
int main()
{
Test test{ 2 };

static_cast<Test>(test);
static_cast<Test&>(test);
static_cast<Test&&>(test);

}

通过断点调试,发现,强转为普通值类型<Test>时,而且是在不进行其他操作的情况下,仍会调用一次普通拷贝构造函数。其他两句不会进行拷贝。

接下来研究其他两个语句——在有变量接收时的行为。

强转为普通值类型

  1. 以普通值类型接收时,会拷贝,调用的是普通拷贝构造;
  2. 不可以左值引用接收普通值类型。
  3. 以右值引用接收时,也会拷贝,调用的是普通拷贝构造。
1
2
3
4
5
6
7
8
int main()
{
Test test{ 2 };

Test test11 = static_cast<Test>(test);
Test & test12 = static_cast<Test>(test);
Test && test13 = static_cast<Test>(test);
}

强转为左值

  1. 以普通值类型接收时,会拷贝,调用的是普通拷贝构造;
  2. 以左值引用接收时,不会拷贝,是高效的用法
  3. 不可以右值引用接收左值。
1
2
3
4
5
6
7
8
int main()
{
Test test{ 2 };

Test test21 = static_cast<Test&>(test);
Test & test22 = static_cast<Test&>(test);
//Test && test23 = static_cast<Test&>(test); // error
}

强转为右值

  1. 以普通值类型接收时,会拷贝,调用右值构造(如果右值构造较好地处理了资源的移动,则较高效。若没有明确写右值构造,则仍会进行普通拷贝,则降低了效率);
  2. 不可以左值引用接收右值。
  3. 以右值引用接收时,不会拷贝,是高效的用法
1
2
3
4
5
6
7
8
int main()
{
Test test{ 2 };

Test test31 = static_cast<Test&&>(test); // decide on Move Constructor
//Test & test32 = static_cast<Test&&>(test); // error
Test && test33 = static_cast<Test&&>(test);
}

其中,以“右值引用类型”接收“强转为右值的对象”(或者std::move后的对象),这个行为其实是对“右值对象”的生命期的延展。如果通过右值引用来修改对象,那么对象源值也会修改。这才是右值引用最本原的功能。

1
2
3
4
5
6
7
8
9
10
int main()
{
Test test{ 2 };
// test : _val : 2

Test && test33 = static_cast<Test&&>(test);
test33.set_value(4);
// test.33 : _val : 4
// test : _val : 4
}

总结

Modern Cpp引入了值类型的细分,不是让你装逼的,也不是为了给程序员增加痛苦的,而是为了让各种类型都有它最好的归宿:

  1. 左值交给左值引用(不会有任何其他多余的操作,高效转手)

  2. 右值交给右值引用(不会有任何其他多余的操作,高效转手)

  3. 右值交给普通类型(调用右值构造,如果右值构造较好地处理了资源的移动,则较高效)

  4. 对于前两种来说,只要引用类型匹配,则无需程序员费心,便可提高效率。

  5. 而对于程序员来说,最要注意的就是第三种情况,即右值构造函数需要程序员来明确写出,指明资源转移的动作,决定了右值交给普通类型的效率。如果没有写右值构造函数,则仍会进行普通拷贝,则降低了效率。

forwarding_reference

形式上和右值引用一样,但性质、行为是未定义的。

  1. 传入模板的类型,是不确定的,需要接收后另外推断。
  2. 传给auto的类型,亦如此,接收后需要推断。

困扰:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
template<typename T>
void test(T const & t)
{

}

int main()
{
const int ca = 10;
int a = 11;
test(ca);
test(a);
test(9);
}

问题:字面常量无法传递给T & t
于是加了T const & t,可以传递了。

但是,导致,如果传的是const int变量,则T后加const与否对应的行为不一样。
总结起来就是:

  1. T后无const时
    1. 传入非常左值,T对应的类型是const int,t对应的类型是const int &
    2. 传入常左值,T对应的类型是int,t对应的类型是int &
    3. 常性在传入前后始终是对应的,很好。
    4. 但是:不能接收右值。
    5. 总结:虽然不怎么通用,但是起码没有很乱。
  2. T后有const时
    1. 能接收右值了,很好。
    2. 但是,无论传入的是常左值、非常左值、右值,最后,T对应的类型都会是int,t对应的类型都会是const int &
    3. 那么就导致:如果传入的是非常左值,由于t被污染成了const int,导致t无法修改。

全新的解决方案

使用T &&这个形式改进T const & 。虽然样子看起来是右值引用,但它不是,因为T是不确定类型,而它又常用在参数转发中,所以起了新名叫”转发引用 (Forwarding Reference)“(也有人叫Universal Reference)。根据接收的值类型的不同,T也会跟着变化。

包容度和T const &一样,既能接收常左值、非常左值,又能接收右值。
最终达到的效果却比T const &更好:

  1. 常左值,T对应的类型是const int &,t对应的类型是const int &
  2. 左值,T对应的类型是int &,t对应的类型是int &
  3. 右值,T对应的类型是int,t对应的类型是int &&
1
2
3
4
5
6
7
8
9
10
11
12
template<typename T>
void test(T && t)
{

}

int main()
{
int a = 11;
test(a);
test(9);
}

引用折叠

Reference Collapsed

模板参数中的&&

虽然经过传入,变成了对应的引用,在函数中也可以修改t的值(const类型除外),但是其实没啥意义,这个转发引用的意义就是在于让你转发的,不是修改值。

1
2
3
4
5
6
7
8
9
10
11
template <typename T>
void test(T && t)
{
t = 8;
}
int main()
{
int a = 11;
test(a); // 可以把本身改为8。
test(9); // 9还是9,不变。 t = 8修改的是副本
}

auto后的&&

1
2
3
4
5
6
7
8
int main()
{
int a = 11;
//int && ri = a; // error 右值引用无法接收左值
//auto & la = 9; // error 左值引用无法接收右值
auto && ra = a; // ok ra : int &
auto && ra2= 9; // ok ra2: int && ra2
}

move

把传入的值类型转换为右值。

1
2
3
4
5
template<typename T>
T&& mymove(T && t)
{
return static_cast<T&&>(t);
}
1
2
T: int & + && -> t: int &
T: int + && -> t: int &&

现在的问题是:传入不同值类型的变量后,行为不一致。若是左值,则最终只能强转为int &;若为右值,则最终只能强转为int &&。现在我们想要追求:无论左右值,最终都转为左值。

那么可以考虑用Traits技术,消除左右值的区别、影响,统一。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
template<typename T>
class RemoveRefTraits
{
public:
using TYPE = T;
};

template<typename T>
class RemoveRefTraits<T&>
{
public:
using TYPE = T;
};

template<typename T>
class RemoveRefTraits<T&&>
{
public:
using TYPE = T;
};

template <typename T>
using RemRef_t = RemoveRefTraits<T>::TYPE;
1
2
3
4
5
template<typename T>
RemRef_t<T>&& mymove(T && t)
{
return static_cast<RemRef_t<T>&&>(t);
}

完美转发

Perfect Forwarding

Cpp_STL_iterator

内容

  1. iterator的分类
  2. iterator的属性
  3. advance–使迭代器移动n步
  4. 针对迭代器的类型萃取
  5. SGI萃取的写法
  6. distance–计算两迭代器之间的距离

iterator

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
#pragma once
namespace xcg
{
struct input_iterator_tag {};
struct output_iterator_tag {};
struct forward_iterator_tag : public input_iterator_tag {};
struct bidirectional_iterator_tag : public forward_iterator_tag {};
struct random_access_iterator_tag : public bidirectional_iterator_tag {};
using ptrdiff_t = int;

template<class _C, class _Ty, class _D = ptrdiff_t, class _Pointer = _Ty*,
class _Reference = _Ty&>
struct iterator
{
typedef _C iterator_category;
typedef _Ty value_type;
typedef _D difference_type;
typedef _Pointer pointer;
typedef _Reference reference;
};
template<class _Ty, class _D = ptrdiff_t>
struct _Forit : public iterator<forward_iterator_tag, _Ty, _D> {};
template<class _Ty, class _D = ptrdiff_t>
struct _Bidit : public iterator<bidirectional_iterator_tag, _Ty, _D> {};
template<class _Ty, class _D = ptrdiff_t>
struct _Ranit : public iterator<random_access_iterator_tag, _Ty, _D> {};

template<class _II, class _D>
inline void __advance(_II& i, _D n, input_iterator_tag)
{
while (n--) ++i;
}
template<class _BI, class _D>
inline void __advance(_BI& i, _D n, bidirectional_iterator_tag)
{
if (n >= 0)
{
while (n--) ++i;
}
else
{
while (n++) --i;
}
}
template<class _RAI, class _D>
inline void __advance(_RAI& i, _D n, input_iterator_tag)
{
i += n;
}
template<class _II, class _D>
inline void advance(_II& i, _D n)
{

}
}
1
2
3
4
5
6
7
8
9
10
template<class _Iterator>
struct iterator_traits
{
iterator_traits() {}
typedef typename _Iterator::iterator_category iterator_category;
typedef typename _Iterator::value_type value_type;
typedef typename _Iterator::difference_type difference_type;
typedef typename _Iterator::pointer pointer;
typedef typename _Iterator::reference reference;
};

advance

1
2
3
4
5
6
inline void advance(_II& i, _D n)
{
iterator_traits<_II>();
typedef typename iterator_traits<_II>::iterator_category cate;
__advance(i, n, cate());
}

萃取器

首先是针对泛型iterator,其是标准的迭代器,重载了*和->运算符。这里的萃取器可容纳的不包括裸指针。

1
2
3
4
5
6
7
8
9
10
template<class _Iterator>
struct iterator_traits
{
iterator_traits() {}
typedef typename _Iterator::iterator_category iterator_category;
typedef typename _Iterator::value_type value_type;
typedef typename _Iterator::difference_type difference_type;
typedef typename _Iterator::pointer pointer;
typedef typename _Iterator::reference reference;
};

接下来是针对裸指针作出的模板特化。裸指针可看作是可随机访问的迭代器。

1
2
3
4
5
6
7
8
9
10
template<class T>
struct iterator_traits<T*>
{
iterator_traits() {}
typedef typename random_access_iterator_tag iterator_category;
typedef typename T value_type;
typedef typename int difference_type;
typedef typename T* pointer;
typedef typename T& reference;
};

接着再次对裸指针的萃取器模板作出部分特化,可处理常裸指针。

1
2
3
4
5
6
7
8
9
10
template<class T>
struct iterator_traits<const T*>
{
iterator_traits() {}
typedef typename random_access_iterator_tag iterator_category;
typedef typename T value_type;
typedef typename int difference_type;
typedef typename const T* pointer;
typedef typename const T& reference;
};

至此代码版本v1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
#pragma once
namespace xcg
{
struct input_iterator_tag {};
struct output_iterator_tag {};
struct forward_iterator_tag : public input_iterator_tag {};
struct bidirectional_iterator_tag : public forward_iterator_tag {};
struct random_access_iterator_tag : public bidirectional_iterator_tag {};
using ptrdiff_t = int;

template<class _C, class _Ty, class _D = ptrdiff_t, class _Pointer = _Ty*,
class _Reference = _Ty&>
struct iterator
{
typedef _C iterator_category;
typedef _Ty value_type;
typedef _D difference_type;
typedef _Pointer pointer;
typedef _Reference reference;
};

template<class _Iterator>
struct iterator_traits
{
iterator_traits() {}
typedef typename _Iterator::iterator_category iterator_category;
typedef typename _Iterator::value_type value_type;
typedef typename _Iterator::difference_type difference_type;
typedef typename _Iterator::pointer pointer;
typedef typename _Iterator::reference reference;
};

template<class T>
struct iterator_traits<T*>
{
iterator_traits() {}
typedef typename random_access_iterator_tag iterator_category;
typedef typename T value_type;
typedef typename int difference_type;
typedef typename T* pointer;
typedef typename T& reference;
};
template<class T>
struct iterator_traits<const T*>
{
iterator_traits() {}
typedef typename random_access_iterator_tag iterator_category;
typedef typename T value_type;
typedef typename int difference_type;
typedef typename const T* pointer;
typedef typename const T& reference;
};

template<class _Ty, class _D = ptrdiff_t>
struct _Forit : public iterator<forward_iterator_tag, _Ty, _D> {};
template<class _Ty, class _D = ptrdiff_t>
struct _Bidit : public iterator<bidirectional_iterator_tag, _Ty, _D> {};
template<class _Ty, class _D = ptrdiff_t>
struct _Ranit : public iterator<random_access_iterator_tag, _Ty, _D> {};

template<class _II, class _D>
inline void __advance(_II& i, _D n, input_iterator_tag)
{
while (n--) ++i;
}
template<class _BI, class _D>
inline void __advance(_BI& i, _D n, bidirectional_iterator_tag)
{
if (n >= 0)
{
while (n--) ++i;
}
else
{
while (n++) --i;
}
}
template<class _RAI, class _D>
inline void __advance(_RAI& i, _D n, input_iterator_tag)
{
i += n;
}
template<class _II, class _D>
inline void advance(_II& i, _D n)
{
iterator_traits<_II>();
typedef typename iterator_traits<_II>::iterator_category cate;
__advance(i, n, cate());
}
}

SGI的做法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//以下三个函数是SGI比较重要的写法模式
template<class _II>
inline typename iterator_traits<_II>::iterator_category
iterator_category(const _II&)
{
typedef typename iterator_traits<_II>::iterator_category cate;
return cate();
}
template<class _II>
inline typename iterator_traits<_II>::value_type*
value_type(const _II&)
{
return static_cast<typename iterator_traits<_II>::value_type*>(0);
}
template<class _II>
inline typename iterator_traits<_II>::difference_type*
difference_type(const _II &)
{
return static_cast<typename iterator_traits<_II>::difference_type*>(0);
}
1
2
3
4
5
6
template<class _II, class _D>
inline void advance(_II& i, _D n)
{
//这里是SGI的写法,使代码更加灵活。
__advance(i, n, iterator_category(i));
}

distance

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
template<class _II>
inline typename iterator_traits<_II>::difference_type
__distance(_II _F, _II _L, input_iterator_tag)
{
typename iterator_traits<_II>::difference_type n = 0;
while (_F != _L)
{
++_F;
++n;
}
return n;
}
template<class _RAI>
inline typename iterator_traits<_RAI>::difference_type
__distance(_RAI _F, _RAI _L, input_iterator_tag)
{
typename iterator_traits<_RAI>::difference_type n = 0;
return _L - _F;
}

template<class _II>
inline typename iterator_traits<_II>::difference_type
distance(_II _F, _II _L, input_iterator_tag)
{
return __distance(_F, _L, iterator_category(_F));
}

迭代器失效的场景

可以指向元素的工具:迭代器、指针和引用。有时,虽然迭代器失效,但指针和引用不一定失效。

除了元素的迭代器失效的问题,还要考虑容器end这个迭代器是否失效的问题。end() 特性:只要元素数变了,之前拿到的 end() 立刻失效。

vector、string

  1. 只要涉及到扩容,全部迭代器都会失效(包括迭代器、指针和引用)。
  2. insert、push_back:之后,插入点(插入后,放的是新元素)到末尾,这一范围全失效(迭代器、指针和引用)。插入点之前的不受影响。
    1. end() 迭代器失效
  3. erase:之后,删除点(删除后,放的是下一个元素)到末尾,这一范围全失效(迭代器、指针和引用)。删除点之前的不受影响。
    1. end() 迭代器失效

deque

deque 不是一整块连续内存,而是多块定长缓冲区 + 一张“块索引表”(map)
插入可能导致扩容(新分配一个连续内存段)。

迭代器里不仅有元素指针,还带着指向块及索引表的位置;一旦在两端扩展需要改动索引表,所有迭代器都会失效。但元素本身通常没搬家,所以引用/指针多数仍然有效(除非你删的就是它,或在中间插入/删除导致元素搬动)。这正是它与 vector/list 最大的差异。

  1. 插入
    1. 首尾插入
      1. 规定所有的迭代器失效(不包括指针、引用)。
      2. 引用/指针通常仍指向原元素,不失效!
        1. 无论是否扩容,其他的元素都是保留在原位置的,因此其他元素的引用/指针不受影响
    2. 在中间插入
      1. 所有全失效(包括迭代器、指针和引用)。
  2. 删除
    1. 首尾元素
      1. 被删除元素的迭代器、指针和引用失效。其他元素不影响!
      2. 如果删除的是首元素,容器的end迭代器不失效(不包括指针、引用)。
        1. 只有这一种情况不使 end() 失效,其他情况都不要用旧的end。
          1. 但是,有可能虽然是pop_front,但恰好容器中只有一个元素,此时end也会失效。
      3. 如果删除的是尾元素,容器的end迭代器会失效(不包括指针、引用)。
    2. 删除中间元素
      1. 所有全失效(包括迭代器、指针和引用)。

list

  1. insert:不会让迭代器失效(所有元素,包括新插入的,都不会失效)。
    1. 但是,插入后,插入点放的是新元素,原来位置的元素被挤到了右边。如果需要继续向右遍历,记得在插入之后单独做一次++
  2. erase:之后,仅对删除的元素的迭代器失效。其他仍有效。

map、multimap、set、multiset

  1. insert:无影响:不会让迭代器、指针、引用失效(所有元素,包括新插入的,都不会失效)。
  2. erase:仅对删除的元素的迭代器、指针、引用失效。其他元素仍有效。

unordered_map、unordered_set

哈希表的插入,可能会存在容器增长导致重新哈希。
插入后,新容器的尺寸超过其容量阈值(计算方式为容器的 bucket_count 乘以其 max_load_factor),则会强制执行重新哈希。删除不涉及重新哈希。
在重新哈希后,容器中的所有迭代器都会失效。(指针、引用仍有效,即使重新哈希)

  1. insert:
    1. 如果不涉及到重新哈希。不会让迭代器、指针、引用失效(所有元素,包括新插入的,都不会失效)。
    2. 如果重新哈希,容器中的所有迭代器失效。(但指针、引用仍有效,即使重新哈希
    3. 元素的指针、引用在所有情况下都保持有效,即使在重新哈希之后也是如此。
  2. erase:仅对删除的元素的迭代器、指针、引用失效。其他元素仍有效。

其他如queue、stack、priority_queue​

没有提供迭代器。不用谈失效的概念。

Array

  1. 固定大小;不会插/删;
  2. 修改元素值使迭代器失效。
  3. swap(array) 不失效迭代器(指向同一块内存)。