问题代码

最近遇到一个模板参数推导的问题,代码如下:

代码

template<typename T>
using scalar = std::enable_if_t<std::is_arithmetic_v<T>, T>;
template<typename T>
void foo(scalar<T> val)
{
...
}
foo(5);

这是我突发奇想写出来的,模板别名 scalar 限制函数参数为数值类型,可以在多处复用,这个代码无法通过编译,编译器提示没有匹配的函数调用。

代码很简单,看起来也没什么不妥,为什么出错了?在我询问了几个常用的 AI 编程助手没有得到满意的解答后(AI 的回答放在文章最后一节),我查阅了一些资料终于弄清了原因。

问题分析

正常的模板实例化过程中,编译器结合模板形参模式和实例化时提供的参数类型,确定一个或一组模板实参类型,将这些实参替换到形参后能够形成与实例化参数相匹配的参数列表。

在 foo(5) 这一调用中,形参是 scalar<T>, 实例化参数是 int 类型,编译器需要确定一个类型 T,使得scalar<T> 匹配 int。

我们来理一下这个过程:

我们会说,这不是一眼就能看出 T 就是 int 嘛,std::enable_if<std::is_arithmetic<T>::value, T>::type成功实例化的结果就是 T 本身。但是站在编译器的角度来看,可不能这样下定论,有些情况下,这部分可能并不是一个可以反推出固定类型的模板,举一个最简单的例子:

代码

template<typename T>
struct wrapper
{
using type = int;
};
template<typename T>
using scalar_confused = typename wrapper<T>::type;
template<typename T>
void foo_confused(scalar_confused<T> val)
{
...
}
foo_confused(5);

这个模板 wrapper 无论用什么类型实例化都能取到 int,也就是说在反推 T 时无法确定一个唯一的类型,这对于编译器来说是无法处理的,于是它实例化不出任何 foo_confused 的实例。

其实 C++ 标准已经对这类问题作出了说明,官方的命名是非推导上下文(non-deduced context):

我们代码的问题就是上图指出的这种情况,如果模板参数只出现在嵌套名称说明符内(即 :: 符号左边的部分),编译器将不会尝试从实例化参数中推导该模板参数,只能使用已经推导出的或显式指定的参数类型。

StackOverflow 上这篇文章(What is a non deduced context?)还有它提到的一些链接把这个概念讲的很清楚。

如果把 scalar 直接展开到使用位置,我们的代码等价于:

代码

template<typename T>
void foo(typename std::enable_if<std::is_arithmetic<T>::value, T>::type val)
{
...
}

这样看来问题就清晰了,我们拐了一个弯创造了一个非推导上下文。编译器在解析到这一步时,就已经拒绝后续的推导了,后面我们关于反推的分析实际都没有发生。

如何解决

问题找到了,那么应该如何解决呢?把推导移到模板参数列表里面,让它在模板参数替换时先推导出来,后面再引用行不行:

代码

template<typename T, typename S = std::enable_if_t<std::is_arithmetic_v<T>, T>>
using scalar = S;

可惜还是不行,而且增加了一层间接,编译器仍旧会失败在相同的位置:

其实解决方法很简单,别名模板只将 scalar<T> 展开为 T,限制条件独立出来作为一个模板参数用于排除不满足条件的实例化类型:

代码

template<typename T, typename = std::enable_if_t<std::is_arithmetic_v<T>>>
using scalar = T;
template<typename T>
void foo(scalar<T> val)
{
...
}
foo(5);

现在调用 foo(5) 时,模板参数推导过程变为:

现在的逻辑变为,任何 scalar<T> 都是 T,但是只有当 T 是算术类型时,scalar<T> 才有效。

有人可能会问,为什么要编写一个这样的模板,而不是直接限制 foo 的参数类型:

代码

template<typename T, typename = std::enable_if_t<std::is_arithmetic_v<T>>>
void foo(T val)
{
...
}

原因前面已经说过,为了复用,scalar<T> 比那一长串检测更简洁。这也是 C++20 concept 的低级实现版本:

代码

template<typename T>
concept scalar = std::is_arithmetic_v<T>;
template<scalar S>
void foo(S val)
{
...
}

其他考量

我在另一篇笔记 限制模板实参类型 中提到过,使用静态断言,可在发生编译错误时提供可读性更高的错误提示,我们的这个例子恰好很符合这一情况:

代码

template<typename T>
struct arithmetic_guard
{
static_assert(std::is_arithmetic_v<T>, "instantiation requires arithmetic type");

using type = T;
}
template<typename T, typename = typename arithmetic_guard<T>::type>
using scalar = T;

改造之后的 scalar 模板如果使用非算术类型进行实例化,就会在编译时指出需要算术类型。

但是静态断言版本存在一个缺点,就是和 SFINAE 不兼容,假如我们想使用 scalar 来编写一个这样的模板:

代码

template<typename T>
auto selected_type_impl(int) -> decltype(std::declval<scalar<T>>(), 0.0);
template
int selected_type_impl(...);
template
using selected_type = decltype(selected_type_impl<T>(0));

如果 scalar 使用静态断言版本实现,那么我们使用非算术类型实例化 selected_type 时,得到的不是一个 int 类型,而是编译错误。因为 SFINAE 的发生时机是在模板参数替换阶段,将判断从模板参数列表移入 static_assert 内的后果就是任何 selected_type_impl 版本都会进行实例化而不会被静默移除,不符合条件的版本将在这一过程中抛出错误。在实际编码时,可根据具体需求选择合适的实现版本。

一些想法

C++ 语言在不断尝试简化模板元编程,C++26 会将静态反射加入语言标准,届时程序的元信息可以直接获取,而不是通过编写七弯八绕的模板来“套出”这些信息。

但是复杂性不是模板元编程的缺陷,相反它能容纳更多的可能性。优秀的模板库在缺乏编译器支持的年代解决问题的思路,很多令人拍案叫绝,成为经典用法甚至推动了语言标准的发展,为更高阶的功能实现奠定基础。

研究并掌握这些复杂巧妙的实现,运用它们在现实问题之前逢山开路遇水搭桥,不断磨炼我们的思维,而不是对它们望而却步。这样在面对语言标准提供的新特性时,我们才能敏锐察觉到它们的设计意图,善于恰当地加以运用,而不是浅尝辄止。

问题总结

思绪飘忽说了一些废话,回到代码的问题上,其实是自己对模板推导规则了解太浅,臆造出一个看似可行的实现,一厢情愿地认为编译器会如此工作。以后还须多多看书和实践,增加知识储备。

AI 有什么表现

出于好奇,我拿这个问题问 AI,看它们能否分析出来,以下是问题的结果。

DeepSeek 的推理能力比较不错,而且完全免费,使用它的深度思考模式提问,得到的结论是两个调用都没有问题:

微软 Edge 自带的免费版 Copilot 作为日常代码问题咨询以及闲聊对象很方便,ThinkDeeper 模式下,它很确定两种都能正确编译:

Claude 生成代码的能力非常强,广受好评,使用它的 concise 模式回答这个问题,它认为两个都不能通过编译:

号称地表最强的 Grok 经过仔细分析后,也没能得出正确的结论:

这三个都没有完全分析正确,这让我有一点意外。清除聊天上下文后拿相同的问题再提问,它们每次几乎都会给出不一样的结论,偶尔能正确地预测。持续聊天并引导它们分析问题,它们中很少能够准确说出问题出在非推导上下文这个点上。

这很难让人完全放心的将代码完全交由 AI 编写,目前来看,使用它们咨询一些编码问题,从中得到启发并亲自确认或者深入研究才是比较稳妥的做法。

C++ 模板参数推导问题小记的更多相关文章

  1. c++11-17 模板核心知识(五)—— 理解模板参数推导规则

    Case 1 : ParamType是一个指针或者引用,但不是universal reference T& const T& T* Case 2 : ParamType是Univers ...

  2. error C2783: 无法为“T”推导 模板 参数

    原则:“模板参数推导机制无法推导函数的返回值类型” 版本一: // 缺少<T> 参数 int n 对比第三个版本( 缺少<T> 参数 T n) ! 编译错误提示: 错误 1 e ...

  3. c++特性:指向类成员的指针和非类型类模板参数和函数指针返回值 参数推导机制和关联型别

    一.c++允许定义指向类成员的指针,包括类函数成员指针和类数据成员指针 格式如下: class A { public: void func(){printf("This is a funct ...

  4. 模板实参推导 & xx_cast的实现

    首先,类模板必须被显式特化.当然了,可以通过一个辅助函数,通过参数类型,返回特化的类模板,来间接处理. 这个技术被广泛应用在ptr_fun, make_pair, mem_fun, back_inse ...

  5. C++11 图说VS2013下的引用叠加规则和模板参数类型推导规则

    背景:    最近在学习C++STL,出于偶然,在C++Reference上看到了vector下的emplace_back函数,不想由此引发了一系列的“探索”,于是就有了现在这篇博文. 前言:     ...

  6. C++17尝鲜:类模板中的模板参数自动推导

    模板参数自动推导 在C++17之前,类模板构造器的模板参数是不能像函数模板的模板参数那样被自动推导的,比如我们无法写 std::pair a{1, "a"s}; // C++17 ...

  7. 零值初始化&字符串常数作为函数模板参数

    1.在定义一个局部变量时,并希望该局部变量的初始化一个值,可以显示调用其默认构造函数,使其值为0(bool类型默认值为false). template <typename T> void ...

  8. C++ 函数模板默认的模板参数

    函数的默认模板参数 你可以为模板参数定义默认值,它们被称作 default template arguments(默认模板参数). 它们甚至可以指向前一个模板参数. 1. 可以直接使用 operato ...

  9. 从零开始学C++之模板(三):缺省模板参数(借助标准模板容器实现Stack模板)、成员模板、关键字typename

    一.缺省模板参数 回顾前面的文章,都是自己管理stack的内存,无论是链栈还是数组栈,能否借助标准模板容器管理呢?答案是肯定的,只需要多传一个模板参数即可,而且模板参数还可以是缺省的,如下: temp ...

  10. Effective Modern C++翻译(2)-条款1:明白模板类型推导

    第一章 类型推导 C++98有一套单一的类型推导的规则:用来推导函数模板,C++11轻微的修改了这些规则并且增加了两个,一个用于auto,一个用于decltype,接着C++14扩展了auto和dec ...

随机推荐

  1. 经典webshell流量特征

    开门见山,不说废话 判断条件 是否符合通信的特征 请求加密的数据和响应包加密的类型一致 是否一直向同一个url路径发送大量符合特征的请求,并且具有同样加密的响应包 一 .蚁剑 特征为带有以下的特殊字段 ...

  2. WindowsPE文件格式入门08.导出表

    https://bpsend.net/thread-377-1-1.html 通过cff , depends灯等软件可以看到dll,导出函数的信息,因为dll中本身就存了这些信息,存了dll中有哪些导 ...

  3. IDEA 调试Java代码的两个技巧

      本文介绍两个使用IDEA 调试Java代码的两个技巧: 修改变量值 使用RuntimeException终止代码执行 修改变量值   在Java代码调试过程中,我们可以修改变量值,使其达到走指定分 ...

  4. 在java中使用lua脚本操作redis

    前言 众所周知,redis可以执行lua脚本,至于为什么要用lua脚本来操作redis,自行百度咯 开始 Bean类 package cn.daenx.myadmin.common.config.re ...

  5. java RSA公私钥生成工具类

    package cn.daenx.my.util; import java.security.*; import java.security.spec.PKCS8EncodedKeySpec; imp ...

  6. Web前端入门第 64 问:JavaScript 几种函数定义方式有什么区别?

    函数 作为 JS 的一等公民,随处可见它的身影. 我理解的它最主要作用就是用来提取重复代码,但凡有 JS 代码需要复制粘贴的时候,那么这时候就可以考虑使用函数封装了. 当函数写在对象中的时候,这时候它 ...

  7. pkuwc and noiwc游记

    博主有奇怪的打字癖好 eg(虽然可能并不会包含其中的某些东西): 豪吃=好吃 事=是 勒si人呐=这是人啊 姜汁=姜经理=副教 pkuwc游记 day -1 浙江的天气预报好神秘,怎么零下到十多度啊, ...

  8. Eplan教程:供电回路、柜内照明、风机空调

    欢迎大家来到"Eplan带你做项目"第四个过程.在第三个过程中,Eplan带你进入了机械设计中的流体工程设计,向大家介绍了Eplan中怎样进行流体工程设计. 在本次过程中,Epla ...

  9. FastAPI安全认证的终极秘籍:OAuth2与JWT如何完美融合?

    扫描二维码 关注或者微信搜一搜:编程智域 前端至全栈交流与成长 发现1000+提升效率与开发的AI工具和实用程序:https://tools.cmdragon.cn/ FastAPI安全与认证实战指南 ...

  10. (一)Qt与Python—PySide的简介及安装

    目录 1.Pyside的简介 2.pyside的安装 3.pyside的Hello world程序 4.参考文献及网站连接 1.Pyside的简介 ​ PySide(在本文中指代PySide2和PyS ...