static_assert是c++11添加的新语法,它可以使我们在编译期间检测一些断言条件是否为真,如果不满足条件将会产生一条编译错误信息。

使用静态断言可以提前暴露许多问题到编译阶段,极大的方便了我们对代码的排错,提前将一些bug扼杀在摇篮里。

然而有时候静态断言并不能如我们预期的那样工作,今天就来看看这些“不正常”的情况,我将举两个例子,每个都有一定的代表性。

为什么我的static_assert不工作

基于静态断言可以在编译期触发,我们希望实现一个模板类,类型参数不能是int,如果违反约定则会给出编译错误信息:

template <typename T>
struct Obj {
static_assert(!std::is_same_v<T, int>, "T 不能为 int");
// do sth with a
}; int main() {
Obj<int> *ptr = nullptr;
}

按照预期,这段代码应该触发静态断言导致无法编译,然而实际运行的结果却是:

g++ --version

g++ (GCC) 12.2.0
Copyright 2022 Free Software Foundation, Inc.
本程序是自由软件;请参看源代码的版权声明。本软件没有任何担保;包括没有适销性和某一专用目的下的适用性担保。 g++ -std=c++20 -Wall -Wextra error.cpp
error.cpp: 在函数‘int main()’中:
error.cpp:10:15: 警告:unused variable ‘ptr’ [-Wunused-variable]
10 | Obj<int> *ptr = nullptr;
| ^~~

事实上除了警告我们ptr没有被使用,程序被正常编译了。换clang是一样的结果。也就是说,static_assert根本没生效。

这不应该啊?我们明明用到了模板,而static_assert作为类的一部分应该也被编译器检测到并被触发才对。

答案就是,static_assert确实没有被触发。

我们先来看看模板类中static_assert在什么时候生效:当需要显式或者隐式实例化这个模板类的时候,编译器就会看见这个静态断言,然后检查断言是否通过。

但我们这里不是有Obj<int> *ptr吗,这难道不会触发实例化吗?答案在c++的标准里:

Unless a class template specialization has been explicitly instantiated (17.7.2) or explicitly specialized (17.7.3), the class template specialization is implicitly instantiated when the specialization is referenced in a context that requires a completely-defined object type or when the completeness of the class type affects the semantics of the program. -- C++17 standard §17.7.1

意思是说,除了显式实例化,模板类还会在需要它实例化的上下文里被隐式实例化,重点在于那个a completely-defined object type

这个“完整的对象类型”是什么呢?很简单,就是一个编译器能看到其完整的类型定义的类型,举个例子:

class A;

class B {
int i = 0;
};

这里的B就是完整的,而A是不完全类型,一个更为人熟知的称法是:class A是类A的前置声明。

因为我们没有A的完整定义,所以我们只能声明A*或者A&类型的变量或者将A作为函数签名的一部分,但不能A instance或者new A。因为前两者是对A的引用,本身不需要知道完整的A是什么样的,而作为函数签名的一部分的时候并不涉及生成实际需要A的代码,因此也可以使用不完全类型。

所以当你定义一个指针或者引用变量,又或者在写函数或者类方法的签名时,他们并不关心前面的类型,只要这个类型的“名字”是存在的且合法的就行,在这些地方并不会导致模板的实例化。所以静态断言没有被触发。

如何修复这个问题?不使用模板类的指针或者引用可以解决大部分问题,把示例里的Obj<int> *ptr = nullptr改成Obj<int> ptr;,立刻就报错了:

g++ -std=c++20 -Wall -Wextra error.cpp

error.cpp: In instantiation of ‘struct Obj<int>’:
error.cpp:12:14: required from here
error.cpp:5:25: 错误:static assertion failed: T 不能为 int
5 | static_assert(!std::is_same_v<T, int>, "T 不能为 int");
| ~~~~~^~~~~~~~~~~~~~~~~
error.cpp:5:25: 附注:‘!(bool)std::is_same_v<int, int>’ evaluates to false
error.cpp: 在函数‘int main()’中:
error.cpp:12:14: 警告:unused variable ‘ptr’ [-Wunused-variable]
12 | Obj<int> ptr;
| ^~~

如果我就要指针呢?那也别用原始指针,请用智能指针:std::unique_ptr<Obj<int>> ptr;:

g++ -std=c++20 -Wall -Wextra error.cpp

error.cpp: In instantiation of ‘struct Obj<int>’:
/usr/include/c++/12.2.0/bits/unique_ptr.h:93:16: required from ‘void std::default_delete<_Tp>::operator()(_Tp*) const [with _Tp = Obj<int>]’
/usr/include/c++/12.2.0/bits/unique_ptr.h:396:17: required from ‘std::unique_ptr<_Tp, _Dp>::~unique_ptr() [with _Tp = Obj<int>; _Dp = std::default_delete<Obj<int> >]’
error.cpp:13:28: required from here
error.cpp:6:25: 错误:static assertion failed: T 不能为 int
6 | static_assert(!std::is_same_v<T, int>, "T 不能为 int");
| ~~~~~^~~~~~~~~~~~~~~~~
error.cpp:6:25: 附注:‘!(bool)std::is_same_v<int, int>’ evaluates to false

模板参数可以是不完整类型,但这里智能指针在析构的时候必须要有完整的类型定义,所以同样触发了类型断言。

这里还有个坑,shared_ptr可以使用不完整类型,但从原始指针构造shared_ptr或者使用它的方法的时候,不接受非完整类型,所以上述代码用shared_ptr是不行的。

unique_ptr虽然也可以使用不完整类型,但必须在智能指针对象被析构的地方可以看到被销毁的类型的完整定义,上面的例子正是在这一步的时候需要类型的完整定义,从而触发隐式实例化,所以触发了静态断言。

如果我一定要用引用呢?通常这没有问题,因为引用需要绑定到一个对象,在函数参数里的话虽然没显式绑定也很可能在函数代码里使用了具体类型的某些方法,这些都需要完整的类型定义,从而触发断言。如果是引用作为类的成员,则必须提供一个构造函数来保证初始化这些引用,所以也没有问题。如果你真的遇到问题了,也可以实现现代c++推崇的值语义和移动语义;否则,你应该思考下自己的设计是否真的合理了。

为什么我的static_assert意外生效了

如果代码里没有实例化模板类的操作(包括显式和隐式)编译器就不会主动去生成模板的实例,是不是可以利用这点来屏蔽某些不需要的模板重载被触发呢?

看个例子:

template <typename T>
struct Wrapper {
// 只接受std::function
static_assert(0, "T must be a std::function");
}; template <typename T, typename... U>
struct Wrapper<std::function<T(U...)>> {
// do sth
}; int f(int i) {
return i*i;
} int main() {
std::function<int(int)> func{f};
Wrapper<decltype(func)> w;
}

我们的Wrapper包装类只接受std::function,其他的类型不能正常工作,在c++20的concept出来之前我们只能用元编程的做法来实现类似的功能。上面的代码与SFINAE手法相比既简单有好理解。

但当我们编译程序:

g++ -std=c++20 -Wall -Wextra error.cpp

error.cpp:6:19: 错误:static assertion failed: T must be a std::function
6 | static_assert(0, "T must be a std::function");
| ^
error.cpp: 在函数‘int main()’中:
error.cpp:20:29: 警告:unused variable ‘w’ [-Wunused-variable]
20 | Wrapper<decltype(func)> w;
| ^

静态断言竟然被触发了?明明我们的代码里没有实例化会报错的那个模板的地方啊。

这时候看代码是没什么用的,需要看标准怎么说的:

If no valid specialization can be generated for a template definition, and that template is not instantiated, the template definition is ill-formed, no diagnostic required.

如果模板没有被使用,也没有任何针对它的特化或部分特化,且模板内部的代码有错误,编译器并不需要给出诊断信息。

重点在于“no diagnostic required”,它说不需要,但也没禁止,所以检测到模板内部的错误并报错也是正常的,不报错也是正常的,这个甚至不算undefined behavior

而且我们的静态断言里所有的内容都能在编译期的初步检查里得到,所以g++和clang++都会产生一条编译错误。

那么怎么解决问题呢?我们要确保模板里的代码至少进行模板类型推导前都是没法判断是否合法的。因此除了没什么语法上明显的错误,我们需要让静态断言依赖模板参数:

template <typename T>
struct Wrapper {
// 只接受std::function
static_assert(sizeof(T) < 0, "T must be a std::function");
};

所有能被sizeof计算的类型大小都不会比0小,所以这个断言总会失败,而且因为我们的断言依赖模板参数,所以除非真的实例化这个模板,否则没法判断代码是不是合法的,因此编译期也不会触发静态断言。

下面是触发断言的结果:

g++ -std=c++20 -Wall -Wextra error.cpp

error.cpp: In instantiation of ‘struct Wrapper<int>’:
error.cpp:20:18: required from here
error.cpp:6:29: 错误:static assertion failed: T must be a std::function
6 | static_assert(sizeof(T) < 0, "T must be a std::function");
| ~~~~~~~~~~^~~
error.cpp:6:29: 附注:the comparison reduces to ‘(4 < 0)’
error.cpp: 在函数‘int main()’中:
error.cpp:20:18: 警告:unused variable ‘w’ [-Wunused-variable]
20 | Wrapper<int> w;
| ^

现在静态断言可以如我们预期的那样工作了。

总结

要想避免static_assert不按我们预期的情况来工作,需要遵守下面的原则:

  • 尽量别用原始指针
  • 尽量少用引用,多使用值语义
  • 模板里要用到的东西尽量要和类型参数相关,尤其是静态断言
参考

https://stackoverflow.com/questions/5246049/c11-static-assert-and-template-instantiation

https://blog.knatten.org/2018/10/19/static_assert-in-templates/

为什么你的static_assert不能按预期的工作?的更多相关文章

  1. 《Django By Example》第四章 中文 翻译 (个人学习,渣翻)

    书籍出处:https://www.packtpub.com/web-development/django-example 原作者:Antonio Melé (译者注:祝大家新年快乐,这次带来<D ...

  2. 关于bug分析与异常处理的一些思考

    前言:工作三年了,工作内容主要是嵌入式软件开发和维护,用的语言是C,毕业后先在一家工业自动化控制公司工作两年半,目前在一家医疗仪器公司担任嵌入式软件开发工作.软件开发中,难免不产生bug:产品交付客户 ...

  3. JavaScript中‘this’关键词的优雅解释

    本文转载自:众成翻译 译者:MinweiShen 链接:http://www.zcfy.cc/article/901 原文:https://rainsoft.io/gentle-explanation ...

  4. [个人翻译]Redis 集群教程(上)

    官方原文地址:https://redis.io/topics/cluster-tutorial  水平有限,如果您在阅读过程中发现有翻译的不合理的地方,请留言,我会尽快修改,谢谢.        这是 ...

  5. C#~异步编程再续~await与async引起的w3wp.exe崩溃

    返回目录 最近怪事又开始发生了,IIS的应用程序池无做挂掉,都指向同一个矛头,async,threadPool,Task,还有一个System.NullReferenceException,所以这些都 ...

  6. Code First :使用Entity. Framework编程(7) ----转发 收藏

    第7章 高级概念 The Code First modeling functionality that you have seen so far should be enough to get you ...

  7. 原生JS:严格模式详解

    严格模式 本文参考MDN做的详细整理,方便大家参考[MDN](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript) 设计目的 设立”严格模式 ...

  8. 初识Docker和Windows Server容器

    概览 伴随着Windows Server 2016 Technical Preview 3 (TP3)版本的发布,微软首次提供了Windows平台下地原生容器.它集成了Docker对Windows S ...

  9. 【转】linux中do{...} while(0)的解释

    在看ldlm的代码过程中遇到了一个很奇怪的问题,有很多宏定义使用了do while(0)这种看起来好像没啥用的代码.然后我就问问师兄,才得知,这种用法很常见,自己又查了一下资料,原来在linux内核代 ...

  10. Nginx-Lua模块的执行顺序

    一.nginx执行步骤 nginx在处理每一个用户请求时,都是按照若干个不同的阶段依次处理的,与配置文件上的顺序没有关系,详细内容可以阅读<深入理解nginx:模块开发与架构解析>这本书, ...

随机推荐

  1. 服务端挂了,客户端的 TCP 连接还在吗?

    作者:小林coding 计算机八股文网站:https://xiaolincoding.com 大家好,我是小林. 如果「服务端挂掉」指的是「服务端进程崩溃」,服务端的进程在发生崩溃的时候,内核会发送 ...

  2. paddleocr安装与图片识别快速开始

    本文首发我的个人博客:paddleocr安装教程快速开始 1. 安装Python环境 wget https://mirrors.huaweicloud.com/python/3.8.5/Python- ...

  3. 记录一下对jdk8后的接口的一些理解

    对于jdk8后的接口,接口中加入了可以定义默认方法和静态方法. 为什么要这样设计呢? 是为了在给接口扩展方法的时候,不会影响已经实现了该接口的类 加入默认方法可以解决:在添加方法的同时,不影响现有的实 ...

  4. 6.Ceph 基础篇 - CephFS 文件系统

    文章转载自:https://mp.weixin.qq.com/s?__biz=MzI1MDgwNzQ1MQ==&mid=2247485294&idx=1&sn=e9039504 ...

  5. Elasticsearch:如何对PDF文件进行搜索

    Elasticsearch 通常用于字符串,数字,日期等数据类型的检索,但是在 HCM.ERP 和电子商务等应用程序中经常存在对办公文档进行搜索的需求.今天的这篇文章中我们来讲一下如何实现 PDF.D ...

  6. Elasticsearch的ETL利器——Ingest节点

    文章转载自: https://mp.weixin.qq.com/s?__biz=MzI2NDY1MTA3OQ==&mid=2247484473&idx=1&sn=1b3b07b ...

  7. ELK 性能优化实践

    文章转载自:https://mp.weixin.qq.com/s?__biz=MzI5MTU1MzM3MQ==&mid=2247489814&idx=1&sn=6916f8b7 ...

  8. MySQL手动恢复数据库测试操作

    事件背景 MySQL数据库每日零点自动全备 某天上午9点,二狗子不小心drop了一个数据库 我们需要通过全备的数据文件,以及增量的binlog文件进行数据恢复 主要思想与原理 利用全备的sql文件中记 ...

  9. 基于MySQL的-u选项实现如何最大程度防止人为误操作MySQL数据库

    在mysql命令加上选项-U后,当发出没有WHERE或LIMIT关键字的UPDATE或DELETE时,MySQL程序就会拒绝执行.那么,我们基于MySQL提供的这项设置,就可以轻松实现如何最大程度防止 ...

  10. flutter系列之:查询设备信息的利器:MediaQuery

    目录 简介 MediaQuery详解 MediaQuery的属性 MediaQuery的构造函数 MediaQuery的使用 总结 简介 移动的开发中,大家可能最头疼的就是不同设备的规格了,现在设备这 ...