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. KingbaseES R6 单节点数据库异机恢复案例

    数据库运行硬件或系统环境如果发生了不可恢复的故障,这时只能采用异机恢复的方式恢复数据库.以下通过例子介绍异机恢复的过程. 一.硬件环境 192.168.237.101 数据库运行源主机 192.168 ...

  2. CentOS7使用yum方式安装Containerd

    # 安装需要的软件包, yum-util 提供yum-config-manager功能,另外两个是devicemapper驱动依赖的 yum install -y yum-utils device-m ...

  3. 解决Nginx+Tomcat中https转http请求问题---解决js加载使用http的问题

    解决js加载使用http的问题 控制台错误提示: Mixed Content: The page at '' was loaded over HTTPS, but requested an insec ...

  4. Elastic:使用Grafana监视 Elasticsearch

  5. gitlab cicd流水线语法

    流水线语法有哪些? 流水线参数列表 Keyword Description script 运行的Shell命令或脚本. image 使用docker映像. services 使用docker服务映像. ...

  6. .NET 反向代理 YARP 代理 GRPC

    前面的 YARP 文档中,介绍了怎么去代理 http,和如何根据域名转发,而在现在微服务的应用是越来越来多了,服务间的调用依靠 http 越来越不现实了,因为 http 多次握手的耗时越发的影响应用的 ...

  7. PAT (Basic Level) Practice 1005 继续(3n+1)猜想 分数 25

    卡拉兹(Callatz)猜想已经在1001中给出了描述.在这个题目里,情况稍微有些复杂. 当我们验证卡拉兹猜想的时候,为了避免重复计算,可以记录下递推过程中遇到的每一个数.例如对 n=3 进行验证的时 ...

  8. 一文读懂Apache Geode缓存中间件

    目录 一.对缓存中间件的诉求 1.1 我们为什么需要缓存中间件 1.2 缓存的分类 1.1.1 弱势缓存 1.1.2 强势缓存 二.什么是Apache Geode 2.1 Apache Geode的架 ...

  9. HDU4734 F(x) (数位DP)

    (如此简短的题目给人一种莫名的压迫感......) 题目中定义一个数的权值求解函数:F(x) = An * 2n-1 + An-1 * 2n-2 + ... + A2 * 2 + A1 * 1. 观察 ...

  10. POJ2486 Apple Tree(树形背包)

    从每个节点u出发后有两种情况:回到u和不回到u. dp数组设为三维,第一维是节点编号,第二维是从该节点开始走的步数,第三维1/0 表示是否回到该节点. 可以回到时:dp[u][j][1]=max(dp ...