LOADING

加载过慢请开启缓存 浏览器默认开启

Okabe's LAB

面试笔记

Interview 2025/3/20

最近在面试阿里云文件存储 C++ 岗位,问了很多底层的问题,以前没有了解过或者印象不清,现在在此记录一番。

std::shared_ptr 和 std::weak_ptr

std::shared_ptr 由指向的数据和控制块组成。控制块维护的是一些元数据,包括强引用计数和弱引用计数。

  • 当强引用计数变为 0 的时候,会释放指向的数据,但是保留控制块,因为可能存在若干 std::weak_ptr 指向该数据。可以通过 std::weak_ptr::expired 判断是否当前数据已过期(即强引用为 0,数据已经被释放)。
  • 当弱引用计数变为 0 的时候,控制块也被释放,此时 std::shared_ptr 走向生命周期的终点。

如何遍历 std::tuple

std::get

auto t = std::make_tuple(1, '123', 2.0);
std::cout << std::get<0>(t) << std::endl;

std::apply

std::apply([](auto&&... args) {
    ((std::cout << args << " "), ...);
}, t);

std::index_sequence

template <typename T, size_t... Is>
void printTuple(const T &tuple, std::index_sequence<Is...>) {
    ((std::cout << std::get<Is>(tuple) << " "), ...);
    std::cout << std::endl;
}

template <typename ...Args>
void printTuple(const std::tuple<Args...> &t) {
    printTuple(t, std::make_index_sequence<sizeof...(Args)>{});
}
printTuple(t);

顺便复习了一下可变参数模板:

template <typename ...Args>
void print(Args... args) {
    // 注意下面这个有无空格的两种写法
    // (std::cout << ... << args);
    ((std::cout << args << " "), ...);
    std::cout << std::endl;
}

template <typename ...Args>
auto sum(Args... args) {
    return (... + args);
}

template <typename T, size_t... Is>
void printElements(const T &v, std::index_sequence<Is...>) {
  print(v[Is]...);
}

template <typename T, typename... Is>
void printElements(const T &v, Is... idx) {
  print(v[idx]...);
}

template <typename T, size_t... Is>
void printTuple(const T &tuple, std::index_sequence<Is...>) {
    ((std::cout << std::get<Is>(tuple) << " "), ...);
    std::cout << std::endl;
}

其他 points

  • 使用数据分区、分片来提升性能;
  • top 指令的 nice 值应该就对应 MLPQ 里的优先级;
  • std::string_view;
  • 为什么 MCS lock 使用 spin lock?
    • 基于 CAS,不涉及上下文切换
    • std::mutex 基于睡眠和唤醒
  • WAL 和 多机分布式备份的优劣对比
  • std::shared_from_this
  • 内外 const
阅读全文

面试常用算法复习

Interview 2025/3/11

快速排序

void quicksort(vector<int> &v, int l, int r) {
  if (l >= r) {
    return;
  }
  int target = v[l];
  int start = l;
  int end = r;
  while (true) {
    while (l < r && v[r] >= target) {
      r--;
    }
    while (l < r && v[l] <= target) {
      l++;
    }
    if (l == r) {
      break;
    }
    swap(v[l], v[r]);
  }
  swap(v[start], v[l]);
  quicksort(v, start, l - 1);
  quicksort(v, l + 1, end);
}

快速幂

int fastPower(int x, int n) {
  int base = x;
  int ans = 1;
  while (n) {
    if (n & 1) {
      ans *= base;
    }
    base *= base;
    n >>= 1;
  }
  return ans;
}

int fastPowerWithMod(int x, int n, int mod) {
  int base = x;
  int ans = 1;
  while (n) {
    if (n & 1) {
      ans = ((ans % mod) * (base % mod)) % mod;
    }
    base = ((base % mod) * (base % mod)) % mod;
    n >>= 1;
  }
  return ans;
}

并查集

int find(vector<int> &fa, int x) {
  return (fa[x] == x) ? x : (fa[x] = find(fa, fa[x]));
}

void merge(vector<int> &fa, int x, int y) { 
    fa[find(fa, x)] = find(fa, y); 
}

#define left_child(x) ((x + 1) * 2 - 1)
#define right_child(x) ((x + 1) * 2)
#define parent(x) ((x + 1) / 2 - 1)

void makeHeap(vector<int> &v) {
  int n = v.size();
  for (int i = 0; i < n; i++) {
    int leftIdx = left_child(i);
    int rightIdx = right_child(i);
    if (leftIdx >= n) {
      break;
    }
    if (leftIdx < n && v[leftIdx] > v[i]) {
      swap(v[leftIdx], v[i]);
    }
    if (rightIdx < n && v[rightIdx] > v[i]) {
      swap(v[rightIdx], v[i]);
    }
  }
}

void push(vector<int> &v, int ele) {
  v.push_back(ele);
  int idx = v.size() - 1;
  while (idx != 0) {
    int parentIdx = parent(idx);
    if (v[parentIdx] < v[idx]) {
      swap(v[parentIdx], v[idx]);
      idx = parentIdx;
    } else {
      break;
    }
  }
}

void siftDown(vector<int> &v) {
  int n = v.size();
  if (n == 0) {
    return;
  }
  int idx = 0;
  while (idx < n) {
    int leftIdx = left_child(idx);
    int rightIdx = right_child(idx);
    int maxIdx = -1;
    int maxV = v[idx];
    if (leftIdx < n && v[leftIdx] > maxV) {
      maxV = v[leftIdx];
      maxIdx = leftIdx;
    }
    if (rightIdx < n && v[rightIdx] > maxV) {
      maxV = v[rightIdx];
      maxIdx = rightIdx;
    }

    if (maxIdx == -1) {
      break;
    }
    swap(v[idx], v[maxIdx]);
    idx = maxIdx;
  }
}

int pop(vector<int> &v) {
  int n = v.size();
  if (n == 0) {
    return 0;
  }

  int ret = v[0];
  swap(v[n - 1], v[0]);
  v.pop_back();
  siftDown(v);
  return ret;
}

void heapSort(vector<int> &v) {
  vector<int> h(v);
  makeHeap(h);
  int n = v.size();
  for (int i = 0; i < n; i++) {
    v[i] = pop(h);
  }
}
阅读全文

C++ Template 阅读笔记(1)

Language 2025/3/2

最近在学习 C++ Template 这本书,补齐了一些模板元编程相关的信息,后续会持续更新。

函数模板

推断函数模板的返回值

假设我们希望计算两个两个类型不相同的变量中的最大值(如 1u 和 2.0f),我们要如何定义函数模板?

// since c++ 14
template <typename T1, typename T2> auto max(T1 x, T2 y) {
  return x > y ? x : y;
}

// here x's type is float
auto x = ::max(1u, 2.0f);

从 c++ 14 开始,我们可以使用 auto 让编译器帮我们自动计算返回值的类型,然而在此之前,我们必须使用 decltypecommon_type 这样的工具来告诉编译器,我们需要返回什么类型:

// c++ 11
template <typename T1, typename T2>
auto max(T1 x, T2 y) -> decltype(x > y ? x : y) {
  return x > y ? x : y;
}

// equal to
template <typename T1, typename T2>
auto max(T1 x, T2 y) -> decltype(true ? x : y) {
  return x > y ? x : y;
}

然而,上述的代码可能面临一些严重的问题。在一些特殊场景下,返回值可能为引用,这时候需要使用类型萃取(type trait)std::decay来将引用“退化”。

// c++ 11
#include <type_traits>
template <typename T1, typename T2>
auto max(T1 a, T2 b) -> typename std::decay<decltype(true ? a : b)>::type {
  return b < a ? a : b;
}

// since c++ 14, simplified
template <typename T1, typename T2>
auto max(T1 a, T2 b) -> std::decay_t<decltype(true ? a : b)> {
  return b < a ? a : b;
}

注意到 c++ 14 对 typename std::decay<decltype(true ? a : b)>::type 这样的写法进行了简化。

这边顺便回顾一下 autodecltype 的区别:

auto 在大部分场景下和模板类型 T 的行为是一致的,会对推导的类型进行退化(decay)。此处的退化是指:

  • 去除 constvolatile 等关键字;
  • 去除引用
  • 将数组退化成指针

decltype 则是老老实实地获取类型,不会进行退化操作。有一个比较有意思的特殊场景:

// will return int &
auto f(int a) -> decltype((a)) { return a; }

虽然上述的用法是不正确的(返回了垂悬引用,dangling reference),但是能表明 decltype 会把 () 括起来的变量视作引用。

言归正传,之前的写法用 decltype 虽然能解决问题,但是未免太麻烦了点,我们可以用 std::common_type_t<T1, T2, ...> 来解决这个问题:

// since c++ 14
template <typename T1, typename T2>
auto max(T1 a, T2 b) -> std::common_type_t<T1, T2> {
  return b < a ? a : b;
}

还有一种写法,实际上 c++ 在实例化模板时,只要能保证能推断出所有的类型即可,我们可以定义一个返回类型 RT:

template <typename RT, typename T1, typename T2> 
RT max(T1 a, T2 b) {
  return b < a ? a : b;
}
auto x = ::max<float>(1u, 2.0f);

这样只需要指定 RT 即可,因为 T1T2 的类型对于编译器而言是已知的。但是下面的写法就会出错:

template <typename T1, typename T2, typename RT>
RT max(T1 a, T2 b) {
  return b < a ? a : b;
}
auto x = ::max<unsigned int, float>(1u, 2.0f);

报错信息为:

Candidate template ignored: couldn’t infer template argument ‘RT’

编译器无法自动推断 RT 的类型,除非使用上述的 auto

c++ 还支持模板类型的默认值,比如常用的 std::priority_queue 实际上有 3 个模板类型。

类模板

c++ 支持使用基础的数值类型(double 除外)作为模板参数:

template <typename T, size_t MaxSize> class Stack {
  array<T, MaxSize> elements;
  Stack(T first) : elements({first}) {}
};

虽然不支持直接使用字符串常量,但是可以通过定义一个变量来解决

template <char const *name> class MyClass {
  ...
};
MyClass<"hello"> x; // ERROR: string literal "hello" not allowed
extern char const s03[] = "hi"; // external linkage 
char const s11[] = "hi"; // internal linkage
int main() {
  MyClass<s03> m03;               // OK (all versions)
  MyClass<s11> m11;               // OK since C++11
  static char const s17[] = "hi"; // no linkage
  MyClass<s17> m17;               // OK since C++17
}

推断指引(Deduction Guides)

template <typename T> class Stack {
  vector<T> elements;

public:
  Stack() = default;
  Stack(T first) : elements({first}) {}
};

Stack(char const *)->Stack<std::string>;

Stack s("123");

如果没有 Stack(char const *)->Stack<std::string>;,s 将被推导为 Stack<const char *>

Concept

template <typename T> struct AccumulationTraits;
template <> struct AccumulationTraits<int> {
  using AccT = int;
  static AccT constexpr zero = 0;
};

template <> struct AccumulationTraits<string> {
  using AccT = string;
  // support since c++ 17
  inline static AccT const zero = "";
};

template <typename T> auto accum(T begin, T end) {
  using AccT = AccumulationTraits<T>::AccT;
  AccT total = AccumulationTraits<T>::zero;

  for (T i = begin; i <= end; i++) {
    total += i;
  }
  return total;
}

int main(int, char **) {
  auto total = accum(1, 100);
  cout << total << endl;
}
template <typename T>
concept Addable = requires(T a, T b) {
                    { a + b } -> std::same_as<T>;
                  };

template <typename T>
  requires Addable<T> && std::is_arithmetic_v<T>
T accum(T begin, T end) {
  T total{};
  for (T i = begin; i <= end; i++) {
    total += i;
  }
  return total;
}

此处也可以把 typename 替换成 Addable 之类的 concept。和 rusttrait 语法非常类似。

阅读全文

Kmesh 环境配置

  • 下载 openEuler 23.03

  • 安装 kernel headers:

    yum install kernel-headers
    
  • 安装 docker:

    参考:https://cloud.tencent.com/developer/article/2383890

    curl -fsSL https://mirrors.tuna.tsinghua.edu.cn/docker-ce/linux/centos/docker-ce.repo -o /etc/yum.repos.d/docker-ce.repo
    sed -i 's#https://download.docker.com#https://mirrors.tuna.tsinghua.edu.cn/docker-ce#' /etc/yum.repos.d/docker-ce.repo
    sed -i 's#$releasever#7#g' /etc/yum.repos.d/docker-ce.repo
    yum install docker-ce-24.0.7
    
  • 修改 docker proxy:

    修改 /etc/docker/daemon.json 为如下格式:

    {
        "proxies": {
            "http-proxy": "http://ip:7890",
            "https-proxy": "http://ip:7890",
            "no-proxy": "127.0.0.1,localhost"
        }
    }
    

    重启 docker:

    systemctl restart docker
    systemctl status docker
    
  • 安装 kind

    wget https://github.com/kubernetes-sigs/kind/releases/download/v0.23.0/kind-linux-amd64
    chmod +x kind-linux-amd64
    mv kind-linux-amd64 /usr/bin/kind
    
  • 安装 istioctl

    curl -L https://istio.io/downloadIstio | sh -
    cd istio-1.22.2/bin
    chmod +x istioctl
    mv istioctl /usr/bin/
    
  • 创建 ambient 模式集群

    kind create cluster --image=kindest/node:v1.23.17 --config=- <<EOF
    kind: Cluster
    apiVersion: kind.x-k8s.io/v1alpha4
    name: ambient
    nodes:
    - role: control-plane
    - role: worker
    - role: worker
    EOF
    
    istioctl install --set profile=ambient --skip-confirmation
    
  • 安装 go

    https://golang.google.cn/doc/install

  • 安装 kubectl

    https://kubernetes.io/zh-cn/docs/tasks/tools/install-kubectl-linux/

  • 安装 kmesh

    参考 https://kmesh.net/en/docs/setup/quickstart/ 即可。

参考:

阅读全文

Istio Hello World

2024/6/4

部署应用

注意,如果用 kind 需要将 HTTP_PROXY 设置为本机的 ip(而非 127.0.0.1),否则访问不到。

按照 istio book info 教程一步步来:https://istio.io/latest/docs/tasks/traffic-management/ingress/ingress-control/

在安装 gateway 之后,由于我们用的是 kind 而不是在云服务商提供的环境,我们需要使用 metallb 来提供外部 ip:https://metallb.universe.tf/installation/

可视化

ssh -NL 20000:localhost:20001 root@10.119.14.80 -p 22
阅读全文

记一次使用 Docker 遇到的奇怪问题

2024/6/3

最近在 jcloud 的服务器上起了一些 docker 容器,发现在容器内部怎么都无法访问公网。最后发现问题是网卡的 MTU 和 docker0 的 MTU 对不上(Docker 默认是 1500,而网卡是 1450)。

类似的问题:https://www.zeng.dev/post/2022-the-docker-mtu-problem/

可以通过 ifconfig -s 查看 MTU 信息。

解决方案:

修改 /etc/docker/damon.json:

{
    "mtu": 1450
}

重启 docker

systemctl restart docker
阅读全文

C++ 笔记(3)—— Effective Modern C++ (3-6 章)

Language 2024/5/29

Item 18 —— std::unique_ptr

std::unique_ptr 支持使用自定义的析构函数,可以传入一个 lambda 函数:

auto delFunc = [](auto* v) {
    std::cout << "Deconstruct for value: " << *v;
};
std::unique_ptr<int, decltype(delFunc)> ptr(new int{123}, delFunc);

这里 decltype 将再次发挥作用。注意,与 std::shared_ptr 不同,这个自定义析构函数的类型是会绑定到 std::unique_ptr 上的(参见模板第二个参数)。

Item 19 —— std::shared_ptr

std::shared_ptr 会额外维护一个控制块:

这个控制块位于堆上。在执行 move 操作的时候,控制块会沿用。当一个 std::shared_ptr 移动构造一个新的 std::shared_ptr 的时候,ref_cnt 不会增加(会将原来的设置为 nullptr)。

不能用一个裸指针初始化多个 std::shared_ptr,这会导致多个控制块,执行多次析构,造成未定义结果

std::shared_ptr 还支持对于每个实例都使用不同的自定义析构函数函数。如果使用了自定义析构函数,就无法用 std::make_shared 生成对应 std::shared_ptr 对象。与 std::unique_ptr 不同,自定义函数不会体现到类型上。
例如:

std::shared_ptr<int> ptr(new int{123}, delFunc);

有时候我们希望,对于一个对象 std::shared_ptr<Object>,我们希望在 Object 的成员函数中创建一个新的,指向自己(this)的 std::shared_ptr<Object>

class Object : public std::enable_shared_from_this<Object> {
   public:
    static std::shared_ptr<Object> Create(int val) {
        return std::shared_ptr<Object>(new Object(val));
    }
    std::shared_ptr<Object> CloneInner() { return shared_from_this(); }
    inline int Val() const { return val; }

    ~Object() { std::cout << "Deconstruct called." << std::endl; }

   private:
    int val;

    // shared_from_this 必须基于已有的 std::shared_ptr 对象。
    // 设置为 private 以防止用户构造一个裸露对象并调用 shared_from_this。
    explicit Object(int val) : val(val) {}
};

int main() {
    auto p = Object::Create(233);
    std::cout << "Value 1: " << p->Val() << std::endl;
    std::cout << "Reference Count: " << p.use_count() << std::endl;

    auto p2 = p->CloneInner();
    std::cout << "Value 2: " << p2->Val() << std::endl;

    std::cout << "Reference Count: " << p2.use_count() << std::endl;
}
阅读全文

C++ 笔记(2)—— Effective Modern C++ (1-3 章)

Language 2024/5/25

以下笔记囊括了前三章我学习到的新知识。

Item 1 —— 模板参数

推导按值传递的模板形式参数类型 T 时,若传入的实参带有 constvolatile 时,将会去掉。
因为按值传递就相当于是对数据进行了拷贝,不应该有上述限制修饰。

若形式参数为 T,传入的数据为函数或者数组,将会退化成对应的指针。除非使用 T &

Item 2 —— auto

一般而言,使用 auto 和模板 T 是一样的。除了以下的情况:

// x 的类别为 std::initializer_list<int>
auto x = {1, 2, 3};

template <typename T>
void func(T x);
// 错误,无法推断 T 的类型
func({1, 2, 3})

// 错误,无法推断类型
// auto 用在函数类型推导时会被当作模板类型推导
auto func() {
    return {1, 2, 3}
}

auto 可以用来推断一些只有编译器清楚的类型,比如 lambda 表达式。

auto x = []() { return "test"; };
auto y = std::function{[]() { return "test"; }};

比起 ystd::function 包裹,使用 auto 的效率更高,内存占用更少。

Item 3 —— decltype

下面的代码将报错:

template <typename Container, typename Index>
auto func(Container& c, Index i) {
    return c[i];
}
std::vector<int> vec{1, 2};
func(vec, 1) = 10;

此处 auto 相当于 T,按值传递。这将会把 vector::operator[] 返回的 T& 的引用给去掉。我们无法给一个右值赋值。

而如下的代码将通过编译:

template <typename Container, typename Index>
decltype(auto) func(Container& c, Index i) {
    return c[i];
}
std::vector<int> vec{1, 2};
func(vec, 1) = 10;

一般而言,不同于 auto 和模板类型推导,decltype 只会鹦鹉学舌。
对于类型为 T 的表达式,除非该表达式仅有一个名字,否则 decltype 总是得出类型 T &

// 此处类型为 int &
decltype(auto) f() {
    int x = 0;
    return (x);
}

Item 5 —— 优先选用 auto 而非显式类别声明

  • 例子 1:vector<int>::size()

    vector<int>::size() 返回的类型是 vector<int>::size_type,用很多常见的显示类别去声明会产生隐式的类型转换,在不同平台可能会有一定问题。

  • 例子 2:遍历 map

    std::map<std::string, int> m;
    for (const std::pair<std::string, int>& p : m) {
    }
    

    这么写看似没有问题,但是实际上真正遍历的时候 p 的类型应该是 std::pair<const std::string, int> &。上述的遍历方式会新建临时变量。这是 map 中的源码:

    typedef pair<const key_type, mapped_type>        value_type;
    ...
    typedef value_type&                              reference;
    

    转换成 auto 之后:

    std::map<std::string, int> m;
    for (const auto& p : m) {
    }
    

    此时 p 的类型为 const std::map::value_type &,符合预期。

  • 例子 3:std::vector<bool>

    std::vector<bool>::operator[] 的返回值不是 bool &,而是 std::vector<bool>::reference。这是因为 bool 在 c++ 里面是用一个 bit 表示的,vector<bool> 不允许直接返回一个 bit 的引用。

    这里如果用 auto 就可能会产生问题,而用 bool 则可以实现隐式类型转换。

Item 7 —— () 与 {}

std::vector<int>{1, 2};
std::vector<int>(1, 2);

两者完全不一样。

只要有机会,总是倾向于使用具有 std::initializer_list 类型的构造函数。

Item 9 —— using 与 typedef

using 支持模板,而 typedef 只能新建一个模板类:

template <typename T>
class Container {
    T x;
};

// 支持
template <typename T>
using ContainerList = std::vector<Container<T>>;
ContainerList<int> list;
// 不支持
template <typename T>
typedef std::vector<Container<T>> ContainerList;

// 支持
template <typename T>
class ContainerList {
   public:
    typedef std::vector<Container<T>> type;
};
typename ContainerList<int>::type list;

这里必须要有 typename,因为有些特例化实现可能会定义一个叫做 type 的成员对象,需要明确告诉编译器这里用的是 type 这个类型。

Item 10 —— 优先使用 enum class

enum class 会限制作用域。普通的 enum 中的枚举值可能会与其他的同名变量产生歧义。而使用 enum class 声明的枚举值只能通过 enum_class_name::value 的方式来访问。

同时,没有 class 限制的枚举值可以发生隐式类型转换,而加了限制的不行:

enum Color1 { red, white };
enum class Color2 { red, white };
// 可以
if (Color1::red > 1.5) {
}
// 不可以
if (Color2::red > 1.5) {
}

enum class 底层使用 int,也可以使用其他类型:

enum class Status: std::uint32_t;

Item 11 —— 使用 delete 而非未定义的私有函数

应该将声明为 delete 的函数声明为 public,这样便于生成更好的报错信息。
假如设置为 private,用户使用了这个函数,一些编译器会先检查函数的可访问性,说该函数是 private,但是更重要的信息显然是该函数被标记为了 delete

Item 12 —— 使用 override 关键字

使用 override 可以让编译器更好地识别函数是否实现了重写。例如 C++ 11 中有一个鲜为人知的特性:

class A {
    void doSomething() &;
    void doSomething() &&;
}

是两个函数,&& 对应右值调用该函数的情况,例如下面的例子:

class A {
   public:
    void doSomething() & { std::cout << "Called first." << std::endl; }
    void doSomething() && { std::cout << "Called second." << std::endl; }
};

A getRVal() {
    return A{};
}

int main() {
    A a{};
    a.doSomething();
    getRVal().doSomething();
}

将输出:

Called first.
Called second.

Item 14 —— noexcept

在保证一定不会产生异常的前提下,能用 noexcept 就用,因为编译器可以对具有该标识的函数进行更好的优化。同时,编译器不会强求 noexcept 一定要调用具有 noexcept 标识的函数。

Item 15 —— constexpr

  • constexpr 对象(包括函数)都具有 const 属性,且由编译期已知的值完成初始化。
  • constexpr 函数在调用的时候,如果传入的实参值是编译器已知的,则会产出编译期结果。否则退化成普通的函数,在运行时计算结果。

好处是可以在编译期就确定一些变量的值,坏处是可能增加编译时间。

Item 16 —— 保证 const 函数的线程安全性

对于被 const 修饰的函数,我们应该保证其线程安全性。例如:

class A {
   private:
    bool is_valid{};
    int val{};
    mutable std::mutex mtx;

   public:
    int getVal() const {
        std::lock_guard<std::mutex> lock_guard(mtx);
        if (is_valid) {
            return val;
        }
        return -1;
    }
};

为了这一点,我们通常会配合 std::mutex 或者 std::atomic 等同步原语。由于 mutex 在上锁的时候会修改其内部状态(该函数未被标记为 const),所以我们需要通过 mutable 告诉 const 函数该变量是会变化的,不应该在函数内限制其不可变。

阅读全文

Ulaanbuh

Life 2024/5/24

I will always cherish my visit in Ulaanbuh desert.

My favourite photo.

The galaxy.

阅读全文

C++ 笔记(1)—— 万能引用/完美转发/引用折叠

Language 2024/3/27

std::move

std::move 的源码长这样:

template <class _Tp>
_LIBCPP_NODISCARD_EXT inline _LIBCPP_INLINE_VISIBILITY _LIBCPP_CONSTEXPR typename remove_reference<_Tp>::type&&
move(_Tp&& __t) _NOEXCEPT {
  typedef _LIBCPP_NODEBUG typename remove_reference<_Tp>::type _Up;
  return static_cast<_Up&&>(__t);
}

它本质上是将一个引用强制转换成了右值引用

万能引用(Universal Reference)

当我们使用 T && x 或者 auto && x 这种需要类型推断的右值形式的时候,就会被编译器作为万能引用。所谓万能引用,就是既能绑定左值引用,又能绑定右值引用。(所以说,在 C++ 中,并不是说两个 & 就代表是右值引用)。

引用折叠(Reference Collapse)

  • 左值-左值 T& &:函数定义的形参类型是左值引用,传入的实参是左值引用
  • 左值-右值 T& &&:函数定义的形参类型是左值引用,传入的实参是右值引用
  • 右值-左值 T&& &:函数定义的形参类型是右值引用,传入的实参是左值引用
  • 右值-右值 T&& &&:函数定义的形参类型是右值引用,传入的实参是右值引用
    但是C++中不允许对引用再进行引用,对于上述情况的处理有如下的规则:

所有的折叠引用最终都代表一个引用,要么是左值引用,要么是右值引用。规则是:如果任一引用为左值引用,则结果为左值引用。否则(即两个都是右值引用),结果为右值引用。

#include <iostream>
template <typename T>
void PrintInner(T &x)
{
    std::cout << "I got a lvalue: " << x << std::endl;
}

template <typename T>
void PrintInner(T &&x)
{
    std::cout << "I got a rvalue: " << x << std::endl;
}

template <typename T>
void Print(T &&x)
{
    PrintInner(x);
    PrintInner(std::move(x));
    PrintInner(std::forward<T>(x));
}

int main()
{
    Print(5);
    std::cout << "-------------------" << std::endl;

    int x = 20;
    Print(x);
    std::cout << "-------------------" << std::endl;

    auto &&x2 = 30;
    std::cout << std::boolalpha << (typeid(x2) == typeid(int &&)) << std::endl;
    std::cout << "-------------------" << std::endl;

    auto &&x3 = x;
    std::cout << std::boolalpha << (typeid(x3) == typeid(int &)) << std::endl;
}

上面这个例子最终会输出:

I got a lvalue: 5
I got a rvalue: 5
I got a rvalue: 5
-------------------
I got a lvalue: 20
I got a rvalue: 20
I got a lvalue: 20
-------------------
true
-------------------
true

观察:

  • 虽然 5 是作为右值被传入函数 Print,但是这个右值被绑定到了形参 x 上,所以 PrintInner 最终调用的是左值引用的版本。
  • std::move 毋庸置疑,本质上就是都强制转换成右值引用。
  • std::forward 为了将 5 继续当作右值转发,我们可以使用 std::forward,也就是 完美转发
  • 事实上,可以将 T && x 和 T &x 合并成 T &&,因为我们有万能引用。

常左值引用可以绑定一个右值。

const std::string && func()  {
    return std::move(std::string("ok"));
}
const std::string &ok = func(); // ok
const std::string &ok = "ok";   // ok

std::string &fail = "fail";
error: non-const lvalue reference to type 'std::string' (aka 'basic_string<char, char_traits<char>, allocator<char>>') cannot bind to a value of unrelated type 'const char[5]'
    std::string &fail = "fail";
阅读全文
1 ... 2 3 4
avatar
Zihong Lin

What I can’t create, I don’t understand.