章节目录

指针、引用和常量的类型自动推导

本节阅读量:

在前面,我们讨论了可以使用auto关键字,让编译器从初始值设定项推断变量的类型:

1
2
3
4
5
6
7
8
int getVal(); // 返回int的一个函数

int main()
{
    auto val { getVal() }; // val 类型推导为 int

    return 0;
}

同时,默认情况下,类型推导将删除const(和constexpr)限定符:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
const double foo()
{
    return 5.6;
}

int main()
{
    const double cd{ 7.8 };

    auto x{ cd };    // double (const 被丢弃)
    auto y{ foo() }; // double (const 被丢弃)

    return 0;
}

通过在定义中添加const(或constexpr)限定符,可以(重新)应用const(或contexpr):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
const double foo()
{
    return 5.6;
}

int main()
{
    constexpr double cd{ 7.8 };

    const auto x{ foo() };  // const double
    constexpr auto y{ cd }; // constexpr double
    const auto z { cd };    // const double

    return 0;
}

类型推导会删除引用属性

除了删除常量限定符外,类型演绎还将删除引用属性:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
#include <string>

std::string& getRef(); // 返回引用

int main()
{
    auto ref { getRef() }; // 类型推导为 std::string (而不是 std::string&)

    return 0;
}

在上面的示例中,尽管函数getRef()返回std::string&,但引用限定符被删除,因此ref的类型被推导为std::string。

就像删除的常量限定符一样,如果希望推导出的类型是引用,可以在定义点重新应用引用:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
#include <string>

std::string& getRef(); // 返回引用

int main()
{
    auto ref1 { getRef() };  // std::string (引用限定符被丢弃)
    auto& ref2 { getRef() }; // std::string& (重新加上引用限定符)

    return 0;
}

顶层const和底层const

顶层(top-level)const 是应用于对象本身的常量限定符。例如:

1
2
const int x;    // const 作用于 x, 所以是顶层const
int* const ptr; // const 作用于 ptr, 所以是顶层const

相反,底层(low-level)const 是应用于被引用或指向的对象的常量限定符:

1
2
const int& ref; // const 作用于被引用的对象, 所以是底层const
const int* ptr; // const 作用于被指向的对象, 所以是底层const

对常量值的引用始终是底层const。指针可以具有顶层、底层或两种const:

1
const int* const ptr; // 左边的 const 是底层, 右边的 const 是顶层

当我们说类型推导删除常量限定符时,它只删除顶层const。不会丢弃底层const。稍后将看到这方面的示例。


类型推导和常量引用

如果初始值设定项是对const(或constexpr)的引用,则首先删除引用(如果类型是auto&,则会重新加上引用),然后从结果中删除任何顶层const。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
#include <string>

const std::string& getConstRef(); // 返回const 引用

int main()
{
    auto ref1{ getConstRef() }; // std::string (先丢弃引用, 然后丢弃顶层const)

    return 0;
}

在上面的示例中,由于getConstRef()返回一个const std::string&,因此首先删除引用,留下一个const std::string。现在是顶层const,因此它也被删除,推导出的类型为std::string。

我们可以重新应用以下任一项或两项:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
#include <string>

const std::string& getConstRef(); // 返回const 引用

int main()
{
    auto ref1{ getConstRef() };        // std::string (引用 和 顶层 const 丢弃)
    const auto ref2{ getConstRef() };  // const std::string (引用丢弃, const 重新设置)

    auto& ref3{ getConstRef() };       // const std::string& (引用重新设置, 底层const 保留)
    const auto& ref4{ getConstRef() }; // const std::string& (引用 和 const 重新设置)

    return 0;
}

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

ref3让事情变得更有趣。通常,引用将首先被删除,但由于重新应用了引用,因此它被保留。这意味着类型仍然是const std::string&。由于它是底层const,不会删除const。因此,推导出的类型是const std::string&。

ref4类似于ref3,只是也重新应用了const限定符。由于类型已经被推导为对const的引用,因此在这里重新应用const是多余的。也就是说,在这里使用const可以明确地表明,结果将是const(而在ref3的情况下,推导结果是const是隐式的,不那么明显)。


constexpr引用

它的工作方式与const引用相同:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
#include <string_view>

constexpr std::string_view hello { "Hello" };

constexpr const std::string_view& getConstRef()
{
    return hello;
}

int main()
{
    auto ref1{ getConstRef() };            // std::string_view (引用 和 顶层 const 丢弃)
    constexpr auto ref2{ getConstRef() };  // constexpr std::string_view (引用丢弃, const 重新设置)

    auto& ref3{ getConstRef() };           // const std::string_view& (引用重新设置, 底层const 保留)
    constexpr auto& ref4{ getConstRef() }; // constexpr const std::string_view& (引用 和 const 重新设置)

    return 0;
}

类型推导和指针

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
#include <string>

std::string* getPtr(); // 返回指针

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

    return 0;
}

还可以将星号与指针类型推导结合使用:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
#include <string>

std::string* getPtr(); // 返回指针

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

    return 0;
}

auto 与 auto* 的区别 (选读)

当我们将auto与指针类型初始值设定项一起使用时,为auto推导的类型包括指针。因此,对于上面的ptr1,auto的替代类型是std::string*。

当我们将auto*与指针类型初始值设定项一起使用时,为auto推导的类型不包括指针——在推导类型之后重新应用指针。因此,对于上面的ptr2,auto的替代类型是std::string,然后重新应用指针。

在大多数情况下,实际效果是相同的(在上例中,ptr1和ptr2都推导为std::string*)。

然而,在实践中,auto和auto*之间存在一些差异。首先,auto*必须解析为指针类型,否则将导致编译错误:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
#include <string>

std::string* getPtr(); // 返回指针

int main()
{
    auto ptr3{ *getPtr() };      // std::string (解引用 getPtr())
    auto* ptr4{ *getPtr() };     // 编译失败 (初始值设定项不是指针)

    return 0;
}

这很有意义:在ptr4情况下,类型自动推导为std::string,然后重新应用指针。因此,ptr4具有类型std::string*,并且不能使用不是指针的初始值设定项来初始化std::string*。

第二,当考虑到const存在的情况下,auto和auto*的行为存在差异。下面介绍这一点。


类型演绎和const指针 (选读)

指针不会被丢弃,我们不必担心这一点。但对于指针,我们既有const指针,也有指向const的指针,也有auto和auto*。就像引用一样,在指针类型推导期间仅删除顶层常量。

让我们从一个简单的例子开始:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
#include <string>

std::string* getPtr(); // 返回指针

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;
}

当使用auto const或const auto时,我们是在说,“使导出的类型成为const”。因此,在ptr1和ptr2的情况下,导出的类型是std::string*,然后应用const,使最终的类型为std::string* const。这类似于const int和int const表示同一事物的方式。

然而,当使用auto*时,const限定符的顺序很重要。左侧的const意味着“使推导指针类型成为指向常量的指针”,而右侧的const则意味着“将推导指针类型变成指针常量”。因此,ptr3最终作为指向常量的指针,而ptr4最终作为指针常量。

现在让我们看一个例子,其中初始值设定项是指向常量的指针常量。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
#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 };  // 错误: const 限定符不能使用两次
    const auto* const ptr8{ ptr }; // const std::string* const

    return 0;
}

ptr1和ptr2的情况很简单。顶层const(指针本身上的const)被删除。未删除所指向对象上的底层const。因此,在这两种情况下,最终的类型都是const std::string*。

ptr3和ptr4的情况也很简单。顶层const被删除,但我们重新设置了它。所指向对象上的底层常量不会被删除。因此,在这两种情况下,最终的类型都是const std::string* const。

ptr5和ptr6,在这两种情况下,顶层常量都会被删除。对于ptr5,auto* const重新应用顶层const,因此最终类型为const std::string* const。对于ptr6,const auto*将const应用于所指向的类型(在本例中,该类型已经是const),因此最终的类型是const std::string*。

在ptr7的情况下,将应用const限定符两次,这是不允许的,并将导致编译错误。

最后,在ptr8情况下,在指针的两侧应用const(这是允许的,因为auto*必须是指针类型),因此结果类型是const std::string* const。


12.12 输入和输出参数

上一节

12.14 第12章总结

下一节