1. std::remove_reference_t和std::remove_cv_t简介
1.1 函数原型及基本解释
心理学告诉我们,人们理解和处理信息的方式主要通过建立模型,而编程也不例外。我们可以把编程理解为是构建计算机世界中的抽象模型。那么,std::remove_reference_t
和std::remove_cv_t
是如何帮助我们构建更加精确和强大的抽象模型呢?它们在我们创建模板时能够帮助我们去除某些我们不希望有的特性,让我们更准确地控制我们的代码。
首先,让我们看一下这两个函数的基本定义:
std::remove_reference_t
:如果类型T是一个左值引用或右值引用,则返回该引用对应的基本类型。否则,返回T本身。std::remove_cv_t
:返回一个没有顶层const和volatile修饰符的T。
它们的应用是在模板编程中,当我们不知道传入类型会是什么样子时,我们可以通过它们确保我们处理的类型不包含引用,const或volatile修饰符,从而更容易进行一些操作。
接下来,让我们看一下这两个函数在代码中的具体表现:
// std::remove_reference_t std::remove_reference_t<int&> // 返回 int std::remove_reference_t<int&&> // 返回 int std::remove_reference_t<int> // 返回 int // std::remove_cv_t std::remove_cv_t<const int> // 返回 int std::remove_cv_t<volatile int> // 返回 int std::remove_cv_t<const volatile int> // 返回 int std::remove_cv_t<int> // 返回 int
我们可以从中看到,无论我们传入什么样的类型,std::remove_reference_t
和std::remove_cv_t
都能成功地去除我们不需要的特性。它们像是我们编程中的"去壳器"和"去皮器",帮助我们去除我们不需要的部分,让我们可以更加专注于我们真正关心的部分。
1.2 在C++11, C++14, C++17, C++20中的表现形式
当我们接触一个新概念时,心理学告诉我们通过在不同上下文环境中观察和应用,可以帮助我们更好的理解和掌握这个概念。std::remove_reference_t
和std::remove_cv_t
就是这样的例子,它们在C++11, C++14, C++17, C++20中有一致的表现形式,从C++14开始,通过_t
后缀形式来使用,使得代码更加清晰易读。
在C++11中,我们需要使用typename std::remove_reference::type
和typename std::remove_cv::type
的形式来获取移除引用或cv修饰符后的类型。在C++14及其后的标准中,我们可以使用std::remove_reference_t
和std::remove_cv_t
的形式。
这种演变不仅简化了语法,也让代码更加易读。让我们通过一些代码示例来看看它们在不同C++标准中的使用:
C++11中的使用
typename std::remove_reference<int&>::type a; // int a typename std::remove_cv<const int>::type b; // int b
C++14及以后的使用
std::remove_reference_t<int&> a; // int a std::remove_cv_t<const int> b; // int b
可以看到,从C++14开始,通过_t
后缀的形式,我们可以更方便地获取到移除引用或cv修饰符后的类型。这种语法的变化让我们的代码更简洁,更容易阅读,也更容易写出类型安全的代码。
2. std::remove_reference_t
2.1 使用场景示例
在C++编程中,我们常常需要对模板类型进行操作。这个过程中,经常会碰到一些无法预知的问题,比如类型可能是引用类型。当我们希望操作的是原始类型而非引用时,std::remove_reference_t
就派上用场了。
场景一:函数参数传递
当我们在一个函数中要处理模板类型,我们可能需要对这个类型进行一些操作。如果我们的函数签名如下:
template<typename T> void foo(T param);
如果这里的T是一个引用类型,我们在处理param时,会直接影响到传递给函数的原始变量。在某些情况下,这可能不是我们所期望的。此时,我们可以使用 std::remove_reference_t
来确保我们处理的是值类型而非引用类型:
template<typename T> void foo(std::remove_reference_t<T> param);
现在,无论T是值类型还是引用类型,我们都会处理它的值类型版本,从而避免了可能的副作用。
我们可以将上述例子与一个心理学中的现象进行类比。当我们与人交往时,对方可能有很多我们不知道的面孔,就像一个类型可能是一个引用类型。我们真正需要的是对方的"原始面孔",就像我们需要原始类型。使用std::remove_reference_t
就像是去掉了对方的"面具",让我们看到了对方的"原始面孔"。
方法 | 应用 | 好处 |
不使用 std::remove_reference_t |
template<typename T> void foo(T param); |
代码简单 |
使用 std::remove_reference_t |
template<typename T> void foo(std::remove_reference_t<T> param); |
避免对原始变量的不期望的修改 |
2.2 在模板元函数中的应用
在 C++ 的元编程中,我们经常会创建一些在编译期执行并生成类型的模板。这些被称为元函数。当你需要去除可能的引用修饰,而只关心原始类型时,std::remove_reference_t
可以被有效地使用在元函数中。
让我们看一个简单的例子:
template<typename T> struct remove_reference_wrapper { typedef std::remove_reference_t<T> type; }; template<typename T> void foo() { typename remove_reference_wrapper<T>::type var; // ... }
在这个例子中,remove_reference_wrapper
是一个元函数,它的作用是去除其模板参数的引用修饰。然后,我们在函数 foo
中使用了这个元函数来生成我们需要的类型。
就像人们可能因为某些原因会改变他们的行为或表达方式,隐藏他们的真实自我,类型也可能被封装或修饰,从而隐藏其原始类型。std::remove_reference_t
正如一个心理咨询师,可以帮助我们看清楚类型的真实面目,进一步理解和使用它。
元函数名称 | 作用 | 用法 |
remove_reference_wrapper |
去除类型的引用修饰 | typename remove_reference_wrapper<T>::type |
2.3 在模板结构体中的应用
在模板结构体中,std::remove_reference_t
可以帮助我们对于模板类型参数更精确地控制。有时,我们想要保证结构体中的类型是原始类型,而不是引用类型。此时,std::remove_reference_t
可以很好地满足这个需求。
例如,让我们考虑一个简单的模板结构体:
template <typename T> struct MyStruct { std::remove_reference_t<T> value; };
在这个例子中,无论 T
是否为引用类型,MyStruct
的 value
成员都将是其非引用版本。这意味着 value
将总是能够存储一个 T
类型的值,而不是一个引用。
这一点就像心理学中的"原生家庭"理论。我们的原生家庭,就像模板参数的原始类型,对我们的成长和形成具有深远影响。std::remove_reference_t
帮助我们回到原始类型,就像心理治疗师通过探索我们的原生家庭来帮助我们理解自己的行为。
结构体名称 | 类型要求 | 用法 |
MyStruct |
无论 T 是否为引用类型,MyStruct::value 都是非引用版本 |
MyStruct<T> obj; obj.value = val; |
2.4 需要注意的使用细节
虽然 std::remove_reference_t
是一个非常有用的工具,但在使用时还是需要注意一些细节:
- 引用折叠规则:在C++中,通过模板参数传递引用的行为可能不是你想象的那样。例如,一个
T&&
类型的模板参数可以匹配左值,也可以匹配右值。这种现象被称为引用折叠。在使用std::remove_reference_t
时,你需要注意这种现象可能会影响到你的代码。 - 与
std::forward
结合使用:如果你正在编写一个接受通用引用(也称为转发引用)的函数,那么你可能需要在函数内部使用std::forward
来正确地转发参数。在这种情况下,使用std::remove_reference_t
可能会打破你的转发逻辑,因为它会移除类型的引用。
这些复杂性就像人类行为中的复杂性一样。我们的行为是由多种因素影响的,包括我们的情绪、环境、生理状况等。理解这些复杂性可以帮助我们更好地理解和预测行为,就像理解 std::remove_reference_t
的细节可以帮助我们编写更好的代码。
细节 | 示例 | 影响 |
引用折叠规则 | T&& 既可以匹配左值,也可以匹配右值 |
代码行为可能与预期不同 |
与 std::forward 结合使用 |
std::remove_reference_t 可能会破坏转发逻辑 |
需要谨慎处理,以保持代码正确性 |
以下是两种情况的示例: |
- 引用折叠规则
在C++中,引用折叠规则是一种特殊的语法规则,它允许通过模板参数传递引用。例如,一个 T&&
类型的模板参数可以匹配左值,也可以匹配右值。这种现象被称为引用折叠。
template<typename T> void foo(T&& param) { // 在这里,param可以是左值引用或右值引用,取决于传入的实参 } int main() { int x = 10; foo(x); // x是左值,所以T&&折叠为int&,param是左值引用 foo(10); // 10是右值,所以T&&保持为int&&,param是右值引用 }
在这个例子中,foo(x)
和foo(10)
都是有效的,尽管x
是左值,10
是右值。这是因为引用折叠规则允许T&&
匹配左值和右值。
- 与
std::forward
结合使用
如果你正在编写一个接受通用引用(也称为转发引用)的函数,那么你可能需要在函数内部使用 std::forward
来正确地转发参数。在这种情况下,使用 std::remove_reference_t
可能会打破你的转发逻辑,因为它会移除类型的引用。
template<typename T> void foo(T&& param) { bar(std::forward<T>(param)); // 正确地转发param到bar baz(std::remove_reference_t<T>(param)); // 错误!移除了param的引用,可能破坏转发逻辑 }
在这个例子中,std::forward(param)
可以正确地将param
的左值性或右值性转发到bar
。但是,std::remove_reference_t(param)
移除了param
的引用,这可能会破坏转发逻辑,导致baz
接收到的参数不是你期望的那样。
2.5 在Qt中的应用与表现形式
在Qt中,std::remove_reference_t
的使用并不多,但在某些情况下,它可以帮助我们处理一些复杂的类型问题。例如,在Qt Multimedia模块的QVideoFrame
类的测试代码中,我们可以看到std::remove_reference_t
的使用。
template<typename T> void tst_QVideoFrame::create_data() { QTest::addColumn<int>("pixelFormat"); QTest::addColumn<QSize>("size"); QTest::addColumn<int>("bytesPerLine"); QTest::addColumn<std::remove_reference_t<T>>("data"); QTest::newRow("Format_ARGB32, 320x240, 1280") << int(QVideoFrame::Format_ARGB32) << QSize(320, 240) << 1280 << std::remove_reference_t<T>(1280 * 240); // ... }
在这个例子中,std::remove_reference_t
用于创建一个新的数据列,这个数据列的类型是模板参数T
的非引用版本。这样做的好处是,我们可以确保data
列存储的数据类型不是引用类型,这样可以避免潜在的引用悬挂问题。
然而,这并不是Qt和std::remove_reference_t
的典型交互方式,因为Qt的大部分代码并不直接依赖于这个标准库模板。在Qt中,我们更常见的是使用Qt自己的类型系统和工具,如QVariant
、QObject
和QMetaObject
等。
总的来说,std::remove_reference_t
在Qt中的使用并不多,但在某些特定的情况下,它可以帮助我们处理一些复杂的类型问题。
【C++ 泛型编程 入门篇】C++元模版中std::remove_reference_t和std::remove_cv_t的运用(二)https://developer.aliyun.com/article/1465306