12-14 使用指针、引用和 const 进行类型推导

在第10.8 课——使用 auto 关键字对对象进行类型推断中,我们讨论了如何auto使用该关键字让编译器从初始化器推断变量的类型:

int main()
{
    int a { 5 };
    auto b { a }; // b deduced as an int

    return 0;
}

我们还注意到,默认情况下,类型推断将从const类型中移除:

int main()
{
    const double a { 7.8 }; // a has type const double
    auto b { a };           // b has type double (const dropped)

    constexpr double c { 7.8 }; // c has type const double (constexpr implicitly applies const)
    auto d { c };               // d has type double (const dropped)

    return 0;
}

可以通过在推导类型的定义中添加const(或)限定符来重新应用 const(或 constexpr):constexpr

int main()
{
    double a { 7.8 };    // a has type double
    const auto b { a };  // b has type const double (const applied)

    constexpr double c { 7.8 }; // c has type const double (constexpr implicitly applies const)
    const auto d { c };         // d is const double (const dropped, const reapplied)
    constexpr auto e { c };     // e is constexpr double (const dropped, constexpr reapplied)

    return 0;
}

类型推导删除引用

除了会丢弃 const 之外,类型推导还会丢弃引用:

#include <string>

std::string& getRef(); // some function that returns a reference

int main()
{
    auto ref { getRef() }; // type deduced as std::string (not std::string&)

    return 0;
}

在上面的例子中,变量ref使用了类型推导。虽然函数getRef()返回的是一个类型std::string&,但引用限定符被省略了,因此的类型ref被推导为类型std::string。

与丢弃类型一样const,如果您希望推导出的类型是一个引用,您可以在定义点重新应用该引用:

#include <string>

std::string& getRef(); // some function that returns a reference

int main()
{
    auto ref1 { getRef() };  // std::string (reference dropped)
    auto& ref2 { getRef() }; // std::string& (reference dropped, reference reapplied)

    return 0;
}

顶级常量和低级常量(Top-level const and low-level const)

顶级const是应用于对象本身的 const 限定符。例如:

const int x;    // this const applies to x, so it is top-level
int* const ptr; // this const applies to ptr, so it is top-level
// references don't have a top-level const syntax, as they are implicitly top-level const

相比之下,低级const是一个 const 限定符,它应用于被引用或指向的对象:

const int& ref; // this const applies to the object being referenced, so it is low-level
const int* ptr; // this const applies to the object being pointed to, so it is low-level

指向常量值的引用始终是低级常量。指针可以有顶级常量、低级常量,或者两者都有

const int* const ptr; // the left const is low-level, the right const is top-level

当我们说类型推导会丢弃 const 限定符时,它只会丢弃顶级 const 限定符。低级 const 限定符不会被丢弃。我们稍后会看到一些例子。

类型推导和常量引用

如果初始化器是对 const 的引用,则首先删除该引用(如果适用,则重新应用),然后从结果中删除任何顶级 const。

#include <string>

const std::string& getConstRef(); // some function that returns a reference to const

int main()
{
    auto ref1{ getConstRef() }; // std::string (reference dropped, then top-level const dropped from result)

    return 0;
}

在上面的例子中,由于getConstRef()返回的是一个const std::string&,因此首先丢弃引用,留下一个const std::string。这个常量现在是顶级常量,因此也被丢弃,从而推断出的类型为std::string。

关键见解:
删除引用可能会将低级常量更改为顶级常量:const std::string&是一个低级常量,但删除引用后会得到const std::string,这是一个顶级常量。

我们可以重新应用引用和/或常量:

#include <string>

const std::string& getConstRef(); // some function that returns a const reference

int main()
{
    auto ref1{ getConstRef() };        // std::string (reference and top-level const dropped)
    const auto ref2{ getConstRef() };  // const std::string (reference dropped, const dropped, const reapplied)

    auto& ref3{ getConstRef() };       // const std::string& (reference dropped and reapplied, low-level const not dropped)
    const auto& ref4{ getConstRef() }; // const std::string& (reference dropped and reapplied, low-level const not dropped)

    return 0;
}

我们在前面的例子中讨论了ref1这种情况。对于ref2,这与情况类似ref1,只是我们重新应用了const限定符,因此推断出的类型是const std::string

接下来ref3的事情变得更有意思了。通常情况下,引用会先被释放,但由于我们重新应用了引用,所以它不会被释放。这意味着类型仍然是const std::string&。而且由于这个常量是低级常量,所以它也不会被释放。因此,推导出的类型是const std::string&。

ref4这种情况与之前ref3类似,只是我们再次应用了const限定符。由于类型已被推断为对 const 的引用,因此const在此处再次应用限定符是多余的。也就是说,const在此处使用限定符可以明确地表明我们的结果是 const 的(而之前的ref3情况中,结果的 const 属性是隐式的,并不明显)。

最佳实践
如果您想要一个常量引用,const即使并非绝对必要,也请重新应用限定符,因为这可以明确您的意图并有助于防止错误。

constexpr 引用呢?

Constexpr 不是表达式类型的一部分,因此它不会被auto推导出来。

提醒:
定义常量引用时(例如const int&),常量适用于被引用的对象,而不是引用本身。
当定义对 const 变量的 constexpr 引用时(例如constexpr const int&),我们需要同时应用constexpr(应用于引用) 和const(应用于被引用的类型)。
这在第 12.4 课中有所介绍——左值引用 const。

#include <string_view>
#include <iostream>

constexpr std::string_view hello { "Hello" };   // implicitly const

constexpr const std::string_view& getConstRef() // function is constexpr, returns a const std::string_view&
{
    return hello;
}

int main()
{
    auto ref1{ getConstRef() };                  // std::string_view (reference dropped and top-level const dropped)
    constexpr auto ref2{ getConstRef() };        // constexpr const std::string_view (reference dropped and top-level const dropped, constexpr applied, implicitly const)

    auto& ref3{ getConstRef() };                 // const std::string_view& (reference reapplied, low-level const not dropped)
    constexpr const auto& ref4{ getConstRef() }; // constexpr const std::string_view& (reference reapplied, low-level const not dropped, constexpr applied)

    return 0;
}

类型推导和指针

与引用不同,类型推导不会丢弃指针:

#include <string>

std::string* getPtr(); // some function that returns a pointer

int main()
{
    auto ptr1{ getPtr() }; // std::string*

    return 0;
}

我们还可以使用星号结合指针类型推导(auto*)来更清楚地表明推导出的类型是指针:

#include <string>

std::string* getPtr(); // some function that returns a pointer

int main()
{
    auto ptr1{ getPtr() };  // std::string*
    auto* ptr2{ getPtr() }; // std::string*

    return 0;
}

关键见解:
类型推断过程中引用会被丢弃,但指针不会被丢弃的原因是引用和指针的语义不同。

当我们评估一个引用时,实际上是在评估被引用的对象。因此,在推断类型时,我们应该推断被引用对象的类型,而不是引用本身的类型,这才是合理的。此外,由于我们推断出的是一个非引用,所以很容易通过 auto& 将其转换为引用。如果类型推断推断出的是一个引用,那么当我们不需要某个引用时,移除它的语法就会复杂得多。

另一方面,指针保存的是对象的地址。当我们对指针求值时,我们求的是指针本身的值,而不是它指向的对象的值(如果需要获取对象的值,我们可以解引用指针)。因此,我们应该推断指针的类型,而不是它指向的对象的类型,这才是合理的。

autoauto*的区别(可选)

当我们使用auto指针类型初始值设定项时,推导出的类型auto包含指针本身。因此,对于上面的代码ptr1,替换后的类型auto是std::string*

当我们使用auto*指针类型初始值设定项时,自动推导出的类型并不包含指针本身——指针会在类型推导完成后重新应用。因此,对于上面的ptr2示例,自动推导出的类型autostd::string,然后指针会被重新应用。

在大多数情况下,实际效果是相同的(并且在上述例子中ptr1,ptr2两者都推导出std::string*)。

然而,两者(auto和auto)在实践中存在一些差异。首先,auto必须解析为指针初始化器,否则会导致编译错误:

#include <string>

std::string* getPtr(); // some function that returns a pointer

int main()
{
    auto ptr3{ *getPtr() };      // std::string (because we dereferenced getPtr())
    auto* ptr4{ *getPtr() };     // does not compile (initializer not a pointer)

    return 0;
}

image

这很合理:在ptr4这种情况下,auto 被推导出为 std::string,然后指针被重新应用。因此, ptr4 的类型为std::string*,我们不能使用非指针类型的初始化器来初始化 std::string*

其次,当我们将 const 引入方程时,auto 和 auto* 的行为存在差异。我们将在下面介绍这一点。

类型推断与常量指针(可选)

由于指针不会被丢弃,我们不必担心这个问题。但对于指针,我们需要考虑常量指针和指向常量的指针这两种情况,以及autovsauto*。就像引用一样,在指针类型推断过程中,只有顶级常量会被丢弃。

我们先来看一个简单的例子:

#include <string>

std::string* getPtr(); // some function that returns a pointer

int main()
{
    const auto ptr1{ getPtr() };  // std::string* const
    auto const ptr2 { getPtr() }; // std::string* const

    const auto* ptr3{ getPtr() }; // const std::string*
    auto* const ptr4{ getPtr() }; // std::string* const

    return 0;
}

当我们使用 const autoauto const 时,我们实际上是在说“将推导出的指针设为常量指针”。因此,对于ptr1ptr2的情况,推导出的类型是std::string*,然后应用 const,最终类型变为 std::string* const。这 const intint const的含义类似。

然而,当我们使用 auto* 时,const 限定符的顺序很重要。左侧的 const 表示“使推导的指针成为指向const的(对象的值不变)指针”,而右侧的 const 表示“使推导的指针类型为常量指针”。因此,ptr3 最终成为指向 const 的指针,而 ptr4 最终成为常量指针。

现在让我们来看一个例子,其中初始化器是指向 const 的 const 指针。

#include <string>

int main()
{
    std::string s{};
    const std::string* const ptr { &s };

    auto ptr1{ ptr };  // const std::string*
    auto* ptr2{ ptr }; // const std::string*

    auto const ptr3{ ptr };  // const std::string* const
    const auto ptr4{ ptr };  // const std::string* const

    auto* const ptr5{ ptr }; // const std::string* const
    const auto* ptr6{ ptr }; // const std::string*

    const auto const ptr7{ ptr };  // error: const qualifer can not be applied twice
    const auto* const ptr8{ ptr }; // const std::string* const

    return 0;
}

image

这ptr1两种ptr2情况都很简单。顶层常量(指针本身的常量)会被丢弃。被指向对象的底层常量不会被丢弃。因此,在这两种情况下,最终类型都是const std::string*。

这ptr3两种ptr4情况也很简单。顶层常量会被丢弃,但我们会重新应用它。被指向对象的底层常量不会被丢弃。因此,在这两种情况下,最终类型都是const std::string* const。

ptr5ptr6的情况与我们在前一个例子中展示的情况类似。在这两种情况下,顶层常量都会被丢弃。对于 ptr5auto* const会重新应用顶层常量,因此最终类型为 const std::string* const。对于ptr6const auto*会将常量应用于被指向的类型(在本例中该类型已经是常量),因此最终类型为 const std::string*

在这种ptr7情况下,我们两次使用了 const 限定符,这是不允许的,会导致编译错误。

最后,在这种ptr8情况下,我们在指针的两侧都应用了 const(这是允许的,因为auto必须是指针类型),所以结果类型是const std::string const。

最佳实践:
如果您想要一个常量指针、指向常量的指针或指向常量的常量指针,const即使并非绝对必要,也请重新应用限定符,因为这可以明确您的意图并有助于防止错误。

提示
auto在推断指针类型时,请考虑使用const。auto在这种情况下使用 const 可以更清楚地表明我们正在推断指针类型,并借助编译器来确保我们不会推断出非指针类型,同时还能让你更好地控制 const。

概括

听到你头疼,真是抱歉。我们快速回顾一下最重要的几点。

顶层常量与底层常量:

  • 顶级常量适用于对象本身(例如const int x或int* const ptr)。
  • 低级常量适用于通过引用或指针访问的对象(例如const int& ref,const int* ptr)。

何种类型推导可以推导:

  • 类型推导首先会丢弃所有引用(除非推导出的类型本身就定义为引用)。对于常量引用,丢弃引用会导致(底层)常量变为顶层常量。
  • 类型推导会丢弃任何顶层常量(除非推导出的类型定义为const或constexpr)。
  • Constexpr 不是类型系统的一部分,因此永远不会被推导出来。它必须始终显式地应用于推导出来的类型。
  • 类型推断不会丢弃指针。
  • 始终明确定义推导出的类型为引用、constconstexpr(视情况而定),即使这些限定符是多余的,因为它们会被推导出来。这有助于防止错误,并清晰地表达你的意图。

类型推导和指针:

  • 使用 auto 时,仅当初始化器是指针时,推导出的类型才是指针。使用 auto* 时,即使初始化器不是指针,推导出的类型也始终是指针。
  • auto const两者const auto都使推导出的指针成为常量指针。没有办法使用auto显式指定底层常量(指向常量的指针)。
  • auto* const这也使得推导出的指针成为常量指针。const auto*使得推导出的指针成为指向常量的指针。如果这些难以记忆,那么, int* const 是一个指向 int 的常量指针,所以它auto* const必须是常量指针。const int*是一个指向常量(int)的指针,所以它const auto*必须是指向常量的指针。
  • 在推断指针类型时,请考虑使用auto*替代auto,因为它允许您显式地重新应用顶层和底层常量,并且如果未推断出指针类型,则会出错。
posted @ 2025-12-19 09:51  游翔  阅读(25)  评论(0)    收藏  举报