【转载】C++代码中将函数返回类型后置有啥好处吗

C++代码中将函数返回类型后置有啥好处吗?

内容如下:

C++代码中将函数返回类型后置有啥好处吗?

  这种语法是 C++11 新增的,学名叫 trailing return type[1]。翻译过来是后置返回类型,trailing 是后面的、拖尾的意思。书写 int func() 比书写 auto func() -> int 省笔墨。强制所有函数的声明改成后者,就如同强制把所有的变量声明类型改成 auto 一样,代码有异味,变得恶心。

C++11 之前

  下面说一下这种语法出现的背景,以及要解决的问题。

  时间倒退到 2011 年以前,在 C++0x(后面正式定名 C++11) 已经明确要添加两项 make C++ great again 的功能——自动类型推导(auto、decltype 关键字)和 lambda 表达式后,接下来就是缝缝补补,修理各种细微问题了。

  在最初的 C/C++,我们写了一个简单的加法函数如下。其中,参数 lhs 是 left hand side,rhs 是 right hand side。lhs 和 rhs 名称是重载二元操作符惯用的名。

int add(const int& lhs, const int& rhs) { return lhs + rhs; }

  接着,我们想支持更多的类型 long、float、double。C 语言只能把每种类型都写一遍,如果不想这样,只能借助宏展开。C++ 相比 C 而言,引入类和模板后,正式分家成为一门强大的计算机语言。对于 C++,这个需求湿湿碎啦!用模板完成,这也是 C++ 语言最擅长的地方。于是修改代码如下:

template <typename T>
T add(const T& lhs, const T& rhs)
{
	return lhs + rhs;
}

  它能处理同种基本数据类型的加法运算,也包括重载了operator + 的类。so far, so good。直到某一天我们发现,它无法完成整数与浮点数的加法。在 C/C++ 语言里,int 和 float 相加,int 会自动类型提升(type promotion 到 float,运算结果返回 float。测试代码如下:

#include <tuple>

template <typename T>
T add(const T& lhs, const T& rhs)
{
	return lhs + rhs;
}
 
int main() 
{
	std::ignore = add(1, 2);            // OK
	std::ignore = add(1.0f, 2.0f);      // OK
//	std::ignore = add(1, 2.0f);         // error: no matching function for call to 'add(int, float)
	std::ignore = add<float>(1, 2.0f);  // OK

	return 0;
}

  代码有一处知识点。std::ignore 本是在 C++11 引入给 std::tuple 用的。然后发现,如果不关心返回值,就可以丢给它,根本不用担心变量重复定义问题。不关心返回值时,过去的做法是用 (void)var; 来消除 warning: unused variable 'var' [-Wunused-variable]。现在(Scott Meyers 在他的书 Effective Modern C++,也就是 C++11 之后)的做法是 std::ignore。将来(C++17)的做法是 [[maybe_unused]]。

  回过神来。上述代码因为参数用了 const 引用,模板的类型带入 T = int 或 T = float 都会失败。虽然 SFINAE(Substitution failure is not an error),但是最终没有一个可匹配上的函数那就是 error。这种情况还可以挽救——第四行调用,显式指定模板类型 add<float>() 让编译通过。

C++11 之后

  救世主在哪里?

  C++ 从 2003 到 2011,停滞了 8 年,是需要革新了。C++ 在 C++11 标准里,强行回收了在 C 里几乎从来没用到过的关键字 auto,让 auto 的生命得到完美绽放。在赋值语句里,auto 可以从等号右边的类型推导出左边的类型。在规范书写的函数里,可以从函数里的 return 语句推导返回类型。下面列出了返回 void 类型的正确和错误写法。

	void fun0() { return;        }  // OK
//	auto fun1() { return void;   }  // Error
	auto fun2() { return void(); }  // OK
	auto fun3() { return;        }  // OK

  有了 auto 可用,我们接着改进代码。我们用两种类型的模板试试,至于返回类型?让它自动推导吧~

template <typename TL, typename TR>
auto add(const TL& lhs, const TR& rhs)
{
    return lhs + rhs;
}

  如果用 C++11 编译器,会报错如下。用 C++14 以后的编译器,是完全通过编译的。C++14 是 C++11 的小更新(比如扩宽了 auto、constexpr 的使用场景),没有增加大的功能。

error: 'auto' return without trailing return type; deduced return types are a C++14 extension.

  错误讲得明明白白,在 C++11, auto 必须跟 trailing return type 配对使用。引入 decltype 关键字,继续修改代码如下。我在下面的回答里提到了 decltype 关键字,可以去围观。

c++为什么变量可以取名final?

template <typename TL, typename TR>
auto add(const TL& lhs, const TR& rhs) -> decltype(lhs + rhs)
{
    return lhs + rhs;
}

  这也是可以通过编译的。然后我们会想,能不能将 decltype(lhs + rhs) 放前面呢?

template <typename TL, typename TR>
decltype(lhs + rhs) add(const TL& lhs, const TR& rhs)
{
	return lhs + rhs;
}

  很抱歉,这样不行,报错为 error: use of undeclared identifier 'lhs' 'rhs', decltype(lhs + rhs) add(const TL& lhs, const TR& rhs)。

  原因是,还没声明类型就在使用变量了,当然不认识变量了!那改成 decltype(TL+ TR) ?不行啊,语法上就说不过去,哪有两个类型相加减的。动动脑筋,改成 decltype(*(TL*)nullptr + *(TR*)nullptr) 就可以了,其他的变种有 decltype(std::declval<TL&>() + std::declval<TR&>())。这算是一种 dirty hack,凭空变出了两种类型的变量。虽然我们知道真正的代码这样写执行时会崩溃,但是 decltype 跟 sizeof 关键字一样,是不会计算括号里的表达式的[2]。比如 decltype(++a) 在完成类型推导,sizeof(++a) 在获取所占空间的大小后,都并不会递增变量 a。

The following operands are unevaluated operands , they are not evaluated:
1. expressions which the typeid operator applies to, except glvalues of polymorphic class types
2. expressions which are operands of the sizeof operator
3. operands of the noexcept operator
4. operands of the decltype specifier

  这里又引出 std::decval[3]知识点, std::declval 是删除了构造函数的类(构造函数会标记为 = delete)的福音。可以看链接里的例子,在不能创建实例的情况下,如何获取类的成员函数的返回类型。

lambda 表达式

  关于 lambda 表达式,是不能在前面声明返回类型的,只能用 trailing return type。

[](int* p) -> int& { return *p; }  // OK
int& [](int* p) { return *p; }     // ill-formed

  那么在什么情况下,需要使用 trailing return type 呢?

  1. 你所在的工程要求是 C++11 标准,不得不用。一定想用的话,得用上面的 dirty hack。
  2. lambda 表达式,想显式写返回类型。这个必须后置了,毫无疑问。
  3. 返回类型经由模板类型推导,且比较长。为了避免头重脚轻,返回类型放后面可增加代码的可读性。
  4. 影响函数的正常声明,需要靠 typedef 或 using 的。(下面给出了例子)

对比以下三种写法,中间的可读性显然好些。当然,在发现的代码规范中,返回类型太长也有单独成行的写法,即下面的第三种,阅读性好像没那么好。

template <typename TL, typename TR>
decltype(TL::A::REALLY::LONG::value + TR::A::REALLY::LONG::value) add(const TL& lhs, const TR& rhs)
{
	return lhs + rhs;
}

template <typename TL, typename TR>
auto add(const TL& lhs, const TR& rhs) -> decltype(TL::A::REALLY::LONG::value + TR::A::REALLY::LONG::value) 
{
	return lhs + rhs;
}

template <typename TL, typename TR>
decltype(TL::A::REALLY::LONG::value + TR::A::REALLY::LONG::value)
add(const TL& lhs, const TR& rhs)
{
	return lhs + rhs;
}

  下面的 fun1 想传递一个 int,返回函数指针。为了聚焦问题,我们这里直接返回 nullptr 而没有写实际内容。可惜 fun1 编译不过,报错 error: expected unqualified-id before ')' token。fun2 利用 typedef FunctionPointer ,完成此项功能。再看 fun3,用上后置返回类型,也能顺利通过。且少了 FunctionPointer 的笔墨,看着也清爽。

int(*)(int) fun1(int)
{
    return nullptr;
}

using FunctionPointer = int(*)(int);  // typedef int(*FunctionPointer)(int);
FunctionPointer fun2(int)
{
    return nullptr;
}

auto fun3(int) -> int(*)(int)
{
    return nullptr;
}

  很多时候,我们会在类 A 里定义类、枚举等限定作用于的类型 B,外部人员使用的时候,需要写成 A::B 的形式。让后置类型,可以丢掉外面 A:: 这个修饰,尤其是 A 比较长时,会影响代码阅读体验。下面是代码演示,make() 是具体实现,需二选一,看你选择红色药丸, 还是蓝色药丸?

class Matrix
{
public:
	enum class Pill
	{
		RED,
		BLUE,
	};
	
public:
	Pill make();
};

Matrix::Pill Matrix::make() { return Pill::RED; }
auto Matrix::make() -> Pill { return Pill::BLUE; }

  Google 的 C++ Guideline[4] 里,规定了有关 trailing return type 代码的建议写法,可以打开链接,了解大厂的代码规范。上面都讲到了。

  有闲暇时间,可以看看 C++ 之父 Bjarne Stroustrup的网站[5],以及写的书《The Design and Evolution of C++》,了解新功能、新语法孕育的过程。

C++语言的设计和演化

京东

¥52.50

去购买

C++语言的设计和演化

语法

-> 级联

  在 C++11 之前,-> 记号表示指针,--> 是“快速趋近于”[doge],其实是 -- 与 > 的组合。C++11 以后,-> 可以表示后置返回类型语法。这种语法可以像指针一样玩级联吗?即可以写出 A -> B -> C 的形式?答案是可以的。下面代码中 = 右边的第一项可以没有,带星号的项可以无限级联下去。

#include <typeinfo>

using T = auto() -> auto(*)() -> auto(*)() -> auto(*)() -> auto(*)() -> int;

int main()
{
	 std::cout << typeid(T).name() << '\n';
	 
	 return 0;
}

  我们来一层层剥洋葱。最里面的是 auto(*)()->int,后置类型拿掉就是 int (*)()。嗯,这个我们知道,函数指针嘛,我们在 C 语言里经常写 typedef int (*fun)();。让 using T1 = auto(*)()->int,剥掉一层,倒数第二层是 auto(*)()-> T1,它的意思也明了了,一个函数指针,返回的类型也是函数指针。这样一层层下去,我们知道,T 最终也将是个函数指针!

// 加上参数玩一下,没毛病。
using T = auto(*)(int, int, int, int, int) ->
	auto(*)(int, int, int, int) ->
	auto(*)(int, int, int) ->
	auto(*)(int, int) ->
	auto(*)(int) ->
	int;

  然后,写出函数的实现也是可以的。

#include <cstdio>

auto game()
{
    return printf("%s\n", __func__);
}

auto funGame() -> auto(*)() -> int
{
    return game;
}

auto playFunGame() -> auto(*)() -> auto(*)() -> int
{
    return funGame;
}

int main()
{
	 playFunGame()()();  // print "game"
	 return 0;
}

overide/final

  对于函数返回类型后置,需要明确 override / final 两个 context-sensitive 关键字的位置。它俩总是在函数申明(trailing return type 也算函数声明的一部分)的最后。比如成员函数的声明

void foo() const override;
需要写成
auto foo() const -> void override;
而不能写成
auto foo() const override -> void;

注意到 override 的位置,不能放在函数实现的最后,否则会编译报错 error: virt-specifiers in 'foo' not allowed outside a class definition。

其他语言

  末尾也附上其他语言的写法吧,可以对比学习和玩味。函数名称是 add。a 和 b、以及返回值都是 int 类型。注释里标住了语言,括号里的 T 与 object 表示类型和对象,在该语言里的语法要求。很多在弱化类型,把类型 T 置于对象 Object 之后,Python 这样的动态类型语言本来就不用写类型。

int add(int a, int b)                // C、C++、C#、Java (T object)
auto add(int a, int b) -> int        // C++11 trailing return type(T object)
auto add(a -> int, b -> int) -> int  // 好像还没有一种语言这么做? (object -> T)
func  add(b int, c int) int          // Go (object T)
function add(a: Int, b: Int): Int    // Haxe、Kotlin (object: T)
function add(a: number, b: number): number  // TypeScript (object: T)
function add(a, b)                   -- Lua (object)
function add (a As Integer, b As Integer) As Integer REM BASIC (object As T)
fn add(b: i32, c: i32) -> i32        // Rust (object: T)
def add(a: int, b: int) -> int:      # Python (object: T)
func add(a: Int, b: Int) -> int      // Swift (object: T)

编辑于 2024-02-24 · 著作权归作者所有

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mfbz.cn/a/582560.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

质量管理,怎能不知道SPC?

SPC&#xff08;统计过程控制&#xff09;是质量管理的一个重要组成部分&#xff0c;它可以帮助企业更好地控制生产过程、提高产品质量、降低生产成本、增强企业的竞争力。 SPC是一种应用统计技术对过程中的各个阶段进行评估和监控&#xff0c;建立并保持过程处于可接受的并且稳…

深入理解Linux调试工具eBPF和strace、内存泄漏处理、Kubernetes容器调试以及C++协程的崩溃信息收集

在软件开发领域&#xff0c;无论是初级开发者还是资深工程师&#xff0c;都需要面对复杂的调试工作。本文将介绍几个重要的调试工具和技术&#xff0c;并提供实际调试方法的指导&#xff0c;包括Linux环境下的eBPF和strace&#xff0c;内存泄漏问题的处理&#xff0c;Kubernete…

020Node.js的FS模块使用fs.mkdir创建目录

Node.js的FS模块使用fs.mkdir创建目录 //fs.mkdir 创建目录 /*path 将创建的目录路径mode 目录权限&#xff08;读写权限&#xff09;&#xff0c;默认777callback 回调&#xff0c;传递异常参数err*/ const fsrequire(fs);fs.mkdir(./css,(err)>{if(err){console.log(err)…

时间序列模型(含python程序实现)

常用按时间顺序排列的一组随机变量来表示一个随机事件的时间序列&#xff0c;简记为 用表示该随机序列的n个有序观察值&#xff0c;称之为序列长度为n的观察值序列。 常用的时间序列模型 时间序列的预处理 拿到一个观察值序列后&#xff0c;首先要对它的纯随机性和平稳性进行…

PC-3000 Mobile Pro: 智能手机及平板设备数据提取及取证工具

天津鸿萌科贸发展有限公司从事数据安全业务20余年&#xff0c;在数据恢复、数据取证、数据备份等领域有丰富的案例经验、前沿专业技术及良好的行业口碑。同时&#xff0c;公司面向取证机构及数据恢复公司&#xff0c;提供数据恢复实验室建设方案&#xff0c;包含数据恢复硬件设…

书生·浦语 大模型(学习笔记-9)大模型微调的相关概念 预训练 与 微调 全量微调FFT 与 PEFT的区别

目录 一、什么是大模型微调 二、指令微调 三、微调的目的 三、微调的方式 四、微调的步骤 五、微调数据准备 六、微调的数据质量 一、什么是大模型微调 预训练和微调的区别&#xff0c;这个很关键 二、指令微调 这个地方主要是微调的角度不同&#xff0c;简单理解&#…

linux jmeter ant下载并安装【2024-亲测】

环境 centos7 一、下载jmeter 在这里插入代码片wget https://dlcdn.apache.org//jmeter/binaries/apache-jmeter-5.6.3.tgz --no-check-certificate解压 tar -zxvf apache-jmeter-5.6.3.tgz复制到安装目录、设置环境变量 vim /etc/profile添加环境变量&#xff0c;路径改成…

YOLOv8: 快速而准确的对象检测

YOLOv8: 快速而准确的对象检测 背景 对象检测是计算机视觉中的一个关键任务,它可以帮助我们在图像或视频中识别和定位感兴趣的物体。其中,YOLO(You Only Look Once)系列是一类非常出色的实时对象检测算法,以其快速和准确的特点而闻名。YOLOv8是YOLO系列的最新版本,由Ultralyti…

【ESP32S3】使用 Flash 下载工具完成 Flash 加密功能

此篇文档记录通过 Flash 下载工具 完成 Flash 加密 功能的实现&#xff0c;此文档不启用 Flash 加密方案的 NVS 加密。 Flash 加密启动的验证代码&#xff1a;esp-idf/components/bootloader_support/src/flash_encrypt.c Flash 加密测试例程&#xff1a;esp-idf/examples/sec…

【yolov8目标检测部署】TensorRT int8量化

原作者github&#xff1a;https://github.com/xuanandsix/Tensorrt-int8-quantization-pipline/tree/main 改进&#xff1a; 源代码支持的TensorRT版本为7.许多属性已经弃用&#xff1b; 在原有的代码上将支持的TensorRT版本从7改到8. &#xff01;&#xff01;不知道如何安装T…

酷得电子机器狗玩具 MCU方案介绍

机器狗是一种多功能、互动性强的机器人&#xff0c;适合家庭和学校环境。它不仅可以陪伴孩子们玩耍&#xff0c;还能帮助他们学习和成长。功能如下&#xff1a; 关节可动&#xff1a;机器狗的关节设计灵活&#xff0c;可以执行各种动作&#xff0c;如“坐下”、“俯卧撑”、“…

【Ant-Desgin-React 步骤条】步骤条配合组件使用

步骤条配合组件使用 基础使用多分组进度 基础使用 /* eslint-disable no-unused-vars */ import React, { useState } from react import { Button, message, Steps, theme } from antd import After from ./components/after import Now from ./components/now const steps …

纯js对比excel小工具

如何使用JavaScript和xlsx.js实现Excel文件对比&#xff1a;实战指南 在日常办公或数据分析工作中&#xff0c;我们经常需要比较两个Excel文件中的数据差异。手动对比不仅耗时费力&#xff0c;还容易出错。本文将带你通过一个简单的网页应用&#xff0c;利用JavaScript和开源库…

【产品经理】如果人人都是产品经理,那么如何提升自己的不可替代性?

任何职业都需要有危机感&#xff0c;不只是产品经理。 人有生老病死&#xff0c;相应的职场上也有升降变离。当乔布斯站在宇宙之巅望着芸芸众生说“活着就是为了改变世界”的时候&#xff0c;这话着实燃烧了我们一把。随之&#xff0c;马化腾、周鸿祎、张小龙、王小川等汹涌而入…

前端canvas项目实战——在线图文编辑器(九):逻辑画布

目录 前言一、 效果展示二、 实现步骤1. 调整布局&#xff0c;最大化利用屏幕空间2. 添加逻辑画布3. 添加遮罩4. 居中显示逻辑画布5. 一个容易被忽视的bug点 三、Show u the code后记 前言 上一篇博文中&#xff0c;我们实现了一组通用的功能按钮&#xff1a;复制、删除、锁定…

Eclipse内存分析器 Java内存分析工具MAT(Memory Analyzer Tool)的介绍与使用

1.visualvm实时监测 2.Memory Analyzer Tool打开 3.工具的使用可以参考 Java内存分析工具MAT(Memory Analyzer Tool)的介绍与使用 ------------------------ 1.我远程发现是其中一个客户端A请求服务器页面响应&#xff0c;一直得不到响应&#xff0c;然后客户端A一直请求&am…

js 字符串 第一个斜杠前最后一次出现英文字母的位置并添加自定义值,返回新值

要找到字符串中第一个斜杠&#xff08;/&#xff09;前最后一次英文字母出现的位置&#xff0c;可以使用正则表达式配合lastIndexOf方法。以下是实现这一功能的示例代码&#xff1a; 如果是匹配第一个数字前的字母加值可以看这里 function findLastLetterIndexBeforeSlash(str…

外贸旺季外贸人如何做好时间管理

第1步 记住这些原则 50-30-20原则 你工作日里50%的时间应该花在有益于你长期发展目标的事情上&#xff0c;30%的时间应该用于你完成中期(两年左右)目标的事情&#xff0c;20%的时间用于完成未来90天以内需要完成的任务。 “一个篮子”原则 One Bucket 尽可能减少自己接收新任务…

《QT实用小工具·四十六》多边形窗口

1、概述 源码放在文章末尾 该项目实现了可以移动的多边形窗口&#xff0c;项目demo演示如下所示&#xff1a; 项目部分代码如下所示&#xff1a; #include "polygonwindow.h"#include <QBitmap> #include <QQuickItem> #include <QQmlFile> #in…

JAVASE->数据结构|顺序表底层逻辑

✅作者简介&#xff1a;大家好&#xff0c;我是橘橙黄又青&#xff0c;一个想要与大家共同进步的男人&#x1f609;&#x1f609; &#x1f34e;个人主页&#xff1a;再无B&#xff5e;U&#xff5e;G-CSDN博客 目标&#xff1a; 1. 什么是 List 2. List 常见接口介绍 3. …