Item 4: 知道怎么去看推导的类型

本文介绍在不同软件开发阶段查看C++类型推导结果的方法,包括利用IDE编辑器、编译器诊断及运行期输出。并对比了各种方法的优缺点,最终推荐使用Boost.TypeIndex库获取准确的类型信息。

本文翻译自《effective modern C++》,由于水平有限,故无法保证翻译完全正确,欢迎指出错误。谢谢!

博客已经迁移到这里啦

对于推导类型结果的查看,根据不同的软件开发阶段,你想知道的信息的不同,可以选择不同的工具。我们将探讨三种可能性:在你编辑代码时获得类型推导信息,在编译期获得信息,在运行期获得信息。

IDE 编辑器

在IDE中编辑代码常常能显示程序实体(比如,变量,参数,函数等)的类型,只需要你做一些像把光标放在实体上面之类的事。举个例子,给出这样的代码:

const int theAnswer = 42;

auto x = theAnswer;
auto y = &theAnswer;

一个IDE编辑器可能会显示x的推导类型为int,y的推导类型为int*。

为了像这样工作,你的代码肯定或多或少处于编译状态,因为只有C++编译器(或者至少是一个编译器前端)在IDE底层运行才能给IDE提供这样的类型推导信息。如果编译器不能成功分析和执行类型推导来明确你的代码,它就不能显示他推导的类型。

对于一些简单的类型,比如int,IDE给出的信息通常是对的。然而,就像我们马上要看到的那样,当涉及更加复杂的类型时,IDE显示的信息可能就没什么帮助了。

编译器诊断

这里有一个有效的方法,让编译器显示它推导的类型,那就是把这个类型用在会导致编译错误的地方。错误信息报告错误的时候常常会涉及到造成这个错误的类型。

假设,为了举个例子,我们想看看之前例子中的x和y被推导成什么类型。我们可以先声明一个class template但是不去定义它。就好像这样漂亮的代码:

template<typename T>    //只是声明TD
class TD;               //TD == type displayer
                        //类型显示器

尝试实例化这个template将引起错误,因为我们没有相应的定义来实例化。为了看x和y的类型,只要使用它们的类型尝试实例化TD:

TD<decltype(x)> xType;  //引起错误,错误会包含
TD<decltype(x)> yType;  //x和y的类型

我使用variableNameType的形式来命名变量名字,因为它们在错误信息输出的时候出现,并帮我找到我要找的信息。对于上面的代码,我的其中一个编译器的一部分关于类型判断的输出就在下面(我已经高亮显示了类型信息)(译注:就是int和const int *)

error: aggregate 'TD<int> xType' has incomplete type and
    cannot be defined
error: aggregate 'TD<const int *> yType' has incomplete type 
    and cannot be defined

一个不同的编译器提供了相同的信息,但是以不同的形式显示:

error: 'xType' uses undefined class 'TD<int>'
error: 'yType' uses undefined class 'TD<const int *>'

先把格式的不同放在一边,当使用这种方法时,我尝试的所有编译器都产出带有有用类型信息的错误消息。

运行期输出

直到运行期前,printf都不能显示类型信息(并不是说我推荐你使用printf),但是它提供对输出格式的所有控制。这里有个难点,就是如何把你想知道的类型用适合显示的文本来输出。你可能觉得,“没问题,typeid和std::type_info::name会拯救我们。”在我们接着探讨如何查看x和y的类型推导前,你可能觉得我们能像下面这样写:

std::cout<<typeid(x).name()<<'\n';  //显示x的y的类型
std::cout<<typeid(y).name()<<'\n';

这个方法依赖于一个事实,那就是对x和y使用typeid,能产生一个std::type_info的对象,并且这个std::type_info对象有一个成员函数,name,它能产生一个C风格的字符串(也就是一个 const char*)来代替类型的名字。

调用std::type_info::name不能保证返回任何明显易懂的类型,但是实现尽量保证有用。有用的情况是不同的,GNU和Clang编译器报告x的类型是“i”,并且y的类型是“PKi”。如果你学过,这些返回信息将是有意义的,来自编译器的这些输出,“i”意味着“int”,“PK”意味着“point to konst(谐音const)”(两个编译器都支持一个工具,c++filt,这工具能解析这些“残缺的”类型)。微软的编译器产生更明确的输出:x是“int”,y是“int const*”.

因为这些x和y的类型结果都是对的,你可能觉得类型显示的问题已经被解决了,但是别这么轻率。考虑下更加复杂的例子:

template<typename T>            //需要调用的template函数
void f(const T& param);

std::vector<Widget> createVec();//工厂函数

cosnt auto vw = createVec();    //初始化vw

if(!vw.empty()){
    f(&vw[0]);                  //调用f
    ...
}

这段代码涉及了一个user-defined的类型(Widget),一个STL的容器(std::vector)和一个auto变量(vw)。这是一个典型的auto类型,你可能想直观地看一下你的编译器会推导出什么类型。例如,如果能看下template参数类型T和函数参数param的类型将非常棒。

使用粗糙的typeid是很直接的,只要加一些代码到f中来显示你想看的类型:

template<typename T>
void f(cosnt T& param)
{
    using std::cout;

    cout<<"T =      "<< typeid(T).name()<< '\n';    //显示T

    cout<<"param =  "<< typeid(param).name()<< '\n';//显示param
}

GNU和Clang编译器产生的可执行文件产生这样的输出:

T =     PK6Widget
param = PK6Widget

我们已经知道对这些编译器,PK意味着“pointer to cosnt”,所以剩下的谜题就是数字6。它简单地表示class名字(Widget)的长度。所以这些编译器告诉我们T和param都是const Widget*的类型。

微软的编译器同样:

T       = class Widget cosnt *
param   = class Widget cosnt *   

这三个独立的编译器产生了同样的信息暗示结果是准确的。但是看得再仔细一些,在template f中,param的声明类型是const T&。这就是问题所在了,T和param的类型是相同的,这看起来不是很奇怪吗?举个例子,如果T是int,那么param应该是const int&,它们并不是一样的类型。

很不幸,std::type_info::name的结果是不可靠的。在这种情况下,例子中的三个编译器对类型的解读都是错误的。另外,本质上,它们都应该要是错误的,因为std::type_info::name的说明书上说,传入的类型会以传值(by-value)的方式传入一个template函数。就像item 1解释的那样,这意味着如果类型是一个引用,他的引用属性会被忽略,并且在去掉引用属性后,是const(或volatile)的,它的const(或volatile)属性也会被忽略。这就是为什么param的类型(cosnt Widget * const&)被报告称cosnt Widget*。指针的引用属性和const属性都被消除了。

同样不幸的是IDE显示的类型信息也是不可靠的,或者至少说是没有用处。在这个例子中,一个我知道的IDE编辑器报告T的类型是(我不是胡编乱造):

cosnt
std::Simple_types<std::Wrap_alloc<std::_Vec_base_types<Widget,
std::allocator<Widget> >::_Alloc>::value_type>::value_type *

同样的IDE编辑器显示param的类型:

const std::_Simple_types<...>::value_type *const &

这没有T的类型那么吓人,但是中间的“…”让你感到困惑,除非你了解到,这个IDE编辑器说出“我省略所有的T表示的东西”。如果运气好的话,你的开发环境会做一个更好的工作。

如果比起运气,你更倾向于依赖库的话,你将会很高兴知道std::type_info::name和IDE都失败的事情,Boost TypeIndex library(常写作Boost.TypeIndex)却成功了。这个库不是标准C++库的一部分,但是IDE和template TD也不是。另外,事实上Boost库(可以从boost.com获取)是跨平台的,开源的,
只要你在license下设计,即使是最偏执的团队(要求很高的可移植性)也能很容易设计出漂亮的程序。这意味着代码中用了Boost库的可移植性同依赖于标准库的可移植性是几乎相同的。

这里给出我们的函数f怎么用Boost.TypeIndex产出正确的类型信息:

#include <boost/type_index.hpp>
template<typename T>
void f(const T& param)
{
    using std::cout;
    using boost::typeindex::type_id_with_cvr;
    // show T
    cout << "T = "
         << type_id_with_cvr<T>().pretty_name()
         << '\n';
    // show param's type
    cout << "param = "
         << type_id_with_cvr<decltype(param)>().pretty_name()
         << '\n';
    …
}

这段代码工作的方式是,function template boost::typeindex::type_id_with_cvr使用一个类型参数(我们想显示信息的类型)并且不移除const,volatile,或引用属性(因此template名字后面带着with_cvr)。结果是一个boost::typeindex::type_index对象,这个对象有一个pretty_name的成员函数,产生一个std::string对象,里面是我们易读的并且符合具体类型的字符串。

用这个f的实现,再看看那个用typeid会对param产生错误类型信息的函数调用:

std::vector<Widget> createVec();

cosnt auto vw = createVec();

if(!vw.empty()){
    f(&vw[0]);
    ...
}

在GNU和Clang编译器下,Boost.TypeIndex 产生这个(准确的)输出:

T       = Widget const*
param   = Widget const* const&

微软的编译器产生的结果本质上是相同的:

T       = class Widget const*
param   = class Widget const* const&

这样的“几乎一致”是好的,但是记住IDE编辑器,编译器错误消息和像Boost.TypeIndex这样的库是几乎所有的工具你能用来帮助你找出你的编译器推导出来的类型。但是,最后说一句,没有任何东西能替代items 1-3中对类型推导的理解。

            你要记住的事
  • 类型的推导常常能在IDE编辑器,编译器的错误消息,Boost TypeIndex库中看到
  • 一些工具的返回结果可能没有帮助或不准确,所以理解C++的类型推导规则是必不可少的
<think>我们正在处理一个编程错误:'sequence item 4: excepted string, list found'。这个错误通常发生在尝试将列表(list)与字符串连接时,而期望的是字符串。例如,在使用`str.join()`方法时,要求所有元素都是字符串,如果其中包含非字符串(比如列表),就会引发这个错误。 根据错误信息,问题出现在序列(sequence)的第4个元素(索引从0开始,所以实际是第5个元素),期望的是一个字符串,但实际发现的是一个列表(list)。 解决这个问题的关键是将所有元素转换为字符串类型,然后再进行连接操作。 ### 错误示例 假设有如下代码: ```python items = ['a', 'b', 'c', ['d', 'e']] # 注意第4个元素是一个列表 result = ''.join(items) # 这里会报错:sequence item 4: excepted string, list found ``` ### 解决方案 1. **确保序列中所有元素都是字符串**:在连接之前,将每个元素转换为字符串。可以使用列表推导式或map函数来实现。 例如: ```python items = ['a', 'b', 'c', ['d', 'e']] # 使用列表推导式将每个元素转换为字符串 str_items = [str(item) if not isinstance(item, str) else item for item in items] # 或者更通用的方式:无论什么类型都转换为字符串 # str_items = [str(item) for item in items] result = ''.join(str_items) ``` 这样,列表`['d','e']`会被转换成字符串`"['d','e']"`(即字符串形式),然后进行连接。 2. **如果列表元素需要展开**:有时,我们可能希望将嵌套列表中的元素也作为单独的元素连接,而不是将整个列表转成一个字符串。那么我们需要先展开嵌套列表。 例如,将嵌套列表展开成扁平列表: ```python def flatten(lst): for item in lst: if isinstance(item, list): yield from flatten(item) else: yield item items = ['a', 'b', 'c', ['d', 'e']] flattened_items = list(flatten(items)) # 得到 ['a','b','c','d','e'] result = ''.join(flattened_items) # 此时所有元素都是字符串吗?不一定,所以还需要确保是字符串 ``` 注意:展开后,如果元素不全是字符串,还需要转换: ```python flattened_items = [str(x) for x in flatten(items)] ``` 3. **具体问题具体分析**:根据实际代码逻辑,决定是展开嵌套列表还是将整个列表转为字符串。 ### 实际调试步骤 1. 找到报错的位置,通常是`join`函数调用处。 2. 检查传递给`join`的可迭代对象(序列)中的每个元素,特别是错误提示的第4个元素(索引为3)。 3. 确定该元素为什么是列表而不是字符串。可能是之前的代码逻辑错误导致。 4. 根据需求,选择上述两种解决方案之一。 ### 示例修正 假设原始代码: ```python # 错误代码 parts = ['Hello', 'world', 123, ['a', 'b']] s = ''.join(parts) # 报错:sequence item 3: expected string, list found ``` 修正后: ```python # 将每个元素转为字符串 parts = ['Hello', 'world', 123, ['a', 'b']] s = ''.join(str(x) for x in parts) # 结果: "Helloworld123['a', 'b']" ``` 或者,如果希望展开列表: ```python def flatten(items): for item in items: if isinstance(item, list): yield from flatten(item) else: yield item parts = ['Hello', 'world', 123, ['a', 'b']] flat_parts = list(flatten(parts)) # ['Hello', 'world', 123, 'a', 'b'] s = ''.join(str(x) for x in flat_parts) # "Helloworld123ab" ``` ### 注意事项 - 如果列表中有自定义对象,需要确保该对象有合理的`__str__`方法,否则转换成的字符串可能不是期望的结果。 - 在转换时,注意数字类型(如整数、浮点数)会被转换为字符串,但可能格式不符合预期,需要额外格式化。 ### 总结 错误`sequence item 4: excepted string, list found`通常发生在使用`str.join()`时,序列中包含非字符串类型(特别是列表)。解决方案是确保序列中每个元素都是字符串,可以通过遍历转换或者先展开嵌套结构再转换。 §§相关问题§§ 1. 如何避免在列表中出现非字符串元素? 2. 除了`join`方法,还有哪些操作会导致类似类型错误? 3. 如何递归展开嵌套列表(多重嵌套)? 4. 在Python中,如何检查一个变量是否是字符串? </think> ### 解决 "sequence item 4: excepted string, list found" 错误 这个错误通常发生在 Python 中使用 `str.join()` 方法时,序列中包含非字符串元素。错误信息中的 "item 4" 表示第 4 个元素(索引从 0 开始)存在问题。以下是完整的解决方案: #### 错误原因分析 1. **核心问题**:当使用 `''.join(sequence)` 时,序列中所有元素必须是**字符串类型**。如果序列包含列表、元组、字典等非字符串元素,就会触发此错误 2. **错误示例**: ```python # 错误代码:第4个元素是列表 data = ["a", "b", "c", [1, 2], "e"] result = ''.join(data) # 触发错误 ``` #### 解决方案 **方法 1:转换所有元素为字符串(推荐)** ```python # 使用列表推导式转换所有元素 data = ["a", "b", "c", [1, 2], "e"] str_data = [str(item) for item in data] # 关键转换 result = ''.join(str_data) print(result) # 输出: "abc[1, 2]e" ``` **方法 2:展开嵌套列表(如果需要)** ```python # 展开嵌套的列表元素 def flatten(lst): for item in lst: if isinstance(item, list): yield from flatten(item) else: yield str(item) # 确保转换为字符串 data = ["a", "b", "c", [1, 2], "e"] result = ''.join(flatten(data)) print(result) # 输出: "abc12e" ``` **方法 3:使用 map 快速转换** ```python data = ["a", "b", "c", [1, 2], "e"] result = ''.join(map(str, data)) # 使用map转换 print(result) # 输出: "abc[1, 2]e" ``` #### 调试技巧 1. **定位问题元素**: ```python # 检查序列中每个元素的类型 for i, item in enumerate(data): if not isinstance(item, str): print(f"索引 {i} 的类型错误: {type(item)}") ``` 2. **预防措施**: ```python # 在拼接前验证数据类型 if all(isinstance(x, str) for x in data): result = ''.join(data) else: # 自动转换非字符串元素 result = ''.join(str(x) for x in data) ``` #### 实际应用场景 这种错误常见于: 1. 从 CSV/JSON 加载的混合数据类型 2. 网络爬虫获取的异构数据 3. 数据库查询结果的拼接操作 4. 日志文件生成时混合字符串和数字[^1]
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值