C++_tips
文章目录
本篇文章主要是对阅读 google c++ tips 后进行的盲目记录与总结。
tips 1: string_view
将字符串传进函数,一般按如下方式。
| |
其中将std::string转换为const char*需要写成string.c_str()。
将const char*转为std::string,直接传参,但会拷贝临时变量。而转换成string_view则没上述问题。
string_view 变量由一个pointer和length构成(这里可能有点类似切片?)
| |
同时string_view本身不拥有数据,因此有生命周期的概念,需要确保使用时间在原字符串生命期的内部。
当想使用一个字符串数据,但不会改动时,请使用string_view。如果要修改,则显示转化std::string(string_view)。
由于string_view很小,因此采用pass by value的方式。
像const一样使用string_view,函数定义时别用const 限定它(参见tips 109)。
| |
可以这样直接打印string_view。但由于string_view不一定NUL-terminated,因此不要用s.data()。
tips 3: StrCat() & StrAppend()
string concat即std::string::operator+是低效的。
其中
| |
在两参数情况下,方式1和2效率相同。但由于没有进行三参数的重载,方式1会像下面处理。
| |
注意到一点,std::move(temp)+baz自c++11开始等价于std::move(temp.append(baz))。因此可能出现一种情况,分配给temp的初始buffer不够囊括下foobar,则会导致新增的reallocation和copy,因此最坏情况n长度的string需要O(n)重分配时间。
absl::StrCat位于absl/strings/str_cat.h。之后有机会好好介绍下函数的实现方式,这里简要介绍下。
| |
通过设置一个固定大小的数组来存储internal,之后用AlphaNum作为StrCat()和StrAppend()包装的类,用piece_和digits_来存储strings_internal的信息。参看.cc文件的实现,对于方式2实现的原理大概就是将所有string转换成AlphaNum,然后提前计算下所需要的size并创建result字符串,通过指向result末端的指针一一memcpy。总共需要一次预分配内存和多次局部拷贝。
| |
类似的原理实现absl::StrAppend。这俩函数支持int32_t, uint32_t, int64_t, uint64_t, float, double, const char*, string_view这些类型的转化。当然可以看出,节约的时间在于临时量的拷贝和多次重分配。
tips 5:消失的艺术
| |
两种写法产生临时变量,通过c_str()拿到指针。但根据c++ 17标准,
Temporary objects are destroyed as the last step in evaluating the full-expression that (lexically) contains the point where they were created.” (A “full-expression” is “an expression that is not a subexpression of another expression”
因此当语句的赋值结束后,临时变量销毁,生命周期的限制便会产生dangling ptr的问题。
option 1: 持有
不如持有临时变量。临时变量在栈上创建,经过rvo(临时变量的move语义),直接构建,而不需要拷贝临时变量。
| |
option 2: 用引用指向临时变量
根据c++ 17 标准,
The temporary to which the reference is bound or the temporary that is the complete object of a sub-object to which the reference is bound persists for the lifetime of the reference.
用引用持有临时变量,并不会比 option 1更优,一般情况安全。但遇到Exception时,会有dangling reference的风险。
| |
情况取决于编译器是否知晓临时变量内部值的引用需要维持。 当然还有一种方式,不要返回对象。
tips 10: 精简地拆分字符串
通常拆分字符串的函数会因为各种输入参数,输出参数和语义需求而弄出多个版本.
谷歌于是造了一个统一的absl::StrSplit()函数,代码位于absl/strings/str_split.h.
| |
这里简要介绍,通过string_view指向输入的字符串,然后分割出来的string_view根据返回值类型,决定是否拷贝.因此由于底层实现使用string_view,避免了拷贝,所以比较高效。
tips 11: 返回值策略
RVO(return value optimization)是被大部分编译器实现的feature。
| |
由于RVO技术,compiler将调用者obj的地址直接传递给被调用者SomeBigObjectFactory。
那么什么时候compiler不会使用RVO技术呢?
| |
如果调用者重新使用一个值来存储返回值,则不会进行RVO。当然这种情况下,会在move-enabled类型内调用移动语义。
| |
如果被调用者返回多个变量作为返回值,也不会做RVO。如果是使用一个变量但返回多个地方,则会做RVO。
temporaries
此外,RVO不仅命名变量上生效,同样也在临时对象上生效,即调用者返回临时变量的情况。
| |
当调用者立刻使用返回值(被存储在临时对象中)时,RVO也会生效。
记住一句话,当代码需要时copy,就会执行copy,不管copy会不会优化。(意思是,这些地方依然需要copy语义,只是被RVO进行copy优化了 )不要为了高效而牺牲正确性。
简单点,直接在局部函数中返回临时变量。
tips 24: 拷贝
当代码为同一个数据出现两个名字时,那就需要一份拷贝。 如果你避免引入新名字,那么编译器可能会帮你去除掉拷贝。
| |
记住一句话
everything you learned about copies in C++ a decade ago is wrong.
阅读c++ 白皮书的时候也发现,历史遗留问题改动还不小。
tips 36: New Join API
了解下 absl::StrJoin 吧。它支持std::string, absl::string_view, int, double – any type that absl::StrCat() supports。
| |
如果你想join一个StrCat不支持的类型,就添加一个自定义Formatter。
| |
源码在absl/strings/str_join.h。简而言之,就是对 tips 3中提到的strings_internal传递$1参数和Formatter做join计算。最后利用strings_internal拷贝到string。
简而言之,涉及到string操作的建议用absl::string
tips 42:最好用工厂函数初始化方法
如果在当前环境禁用exception,则c++ ctor必须成功,毕竟没有通知caller构造失败的方法了。如果你使用abort,则会使整个程序崩溃,对于产品代码得不偿失。
有一种简单的方式是提供factory function来创建和初始化instance,并返回它的指针或者absl::optional(Tips 123),用null表示失败(option的做法有类型统一的好处)。
| |
Foo::Create()只会暴露出成功初始化的对象,同时也能像初始化方法表达失败。工厂函数的另一个优点是它能返回instances of any subclass of the return type(使用absl::optional作为返回类型当然就不行了)。这允许你使用不同的实现时,而不需要更新用户代码。甚至根据用户输入,动态选择实现类。
这里说的做法大概是根据Create()函数的输入参数进行选择impl,返回参数的类型如果是指针的话,子类型也可以返回。
该方法的缺点是生成的是分配在堆上的对象,对value-like类在栈上工作不友好。当derived class ctor需要初始化base class时,工厂函数不能使用,因此初始化方法在基类的protected API中是必要的。
这里我所理解的工厂函数,是通过某个函数包装原函数的指针。该指针同时包含原函数是否执行成功的状态信息。
tips 45: 库代码中避免 Flags
在产品代码中flags的通常使用,尤其是在库代码中,是一个巨大的错误。
Flags是全局变量时,只会让事情更糟。无法阅读代码知道变量的值,不知道多次版本更迭后flag值是否保持不变。
谨慎使用flag。使用数字flag可以考虑变成compile-time constants。
这里说的flag大概是一些宏定义或者用于标记性质的常量。
tips 49:参数依赖的查找(Argument-Dependent Lookup)_unfinished
一个函数调用表达式,类似func(a,b,c),没有::域名操作符时,称为非限定的(unqualified),此时编译器会进行匹配函数声明的查找。
the set of search scopes is augmented by namespaces associated with the function argument types. This additional lookup is called Argument-Dependent Lookup (ADL).
tips 55: 命名计数和 unique_ptr
通俗说,一个值的 name 表示任何值类型的变量(不是指针,也不是引用),存在在任何作用域且持有某个特别的数据值。(对于专门的C++律师,我们说name一般指的是lvalue)由于unique_ptr特殊行为需求,我们需要确保任何unique_ptr持有的值都只有一个名字。
需要注意的是,C++ 语言委员会为 std::unique_ptr 选择了一个非常恰当的名称。任何存储在unique_ptr的非空指针值 在任何时候都只能出现在一个unique_ptr中。标准库的设计符合这个要求。
在每一行,计算在该点(无论是否在范围内)活动的名称的数量,这些名称引用包含相同指针的 std::unique_ptr。 如果您发现同一指针值属于多个名称的任何行,那就是错误!
| |
在 Simple() 中,用 NewFoo() 分配的唯一指针只有一个可以引用它的名称:AcceptFoo() 中的名称“f”。
将其与 DoesNotBuild() 进行对比:使用 NewFoo() 分配的唯一指针有两个引用它的名称:DoesNotBuild() 的“g”和 AcceptFoo() 的“f”。
这就是常见的唯一性违规。在执行的任何给定点,std::unique_ptr 持有的任何值(或更一般地,任何仅移动类型)都只能由一个不同的名称引用。 任何看起来像引入附加名称的副本都是禁止的,并且不会编译:
| |
即使编译器没有捕捉到你,std::unique_ptr 的运行时行为也会。 任何时候你“超越”编译器(参见 SmarterThanTheCompilerButNot())并引入多个 std::unique_ptr 名称,它可能会编译(目前),但你会遇到运行时内存问题。
那么问题来了:我们如何删除一个名字? C++11 也为此提供了解决方案,形式为 std::move()。
| |
对 std::move() 的调用实际上是一个名称擦除器:从概念上讲,您可以停止将“h”计算为指针值的名称。 这现在通过了 distinct-names 规则:在分配给 NewFoo() 的唯一指针上有一个名称(“h”),并且在对 AcceptFoo() 的调用中再次只有一个名称(“f”)。 通过使用 std::move(),我们保证在为它分配新值之前不会再次读取“h”。
简而言之, 多一个名字便多一分拷贝。unique_ptr只能用一个名字,如果想更换名字,就用move
tips 163: 传递 absl::optional 参数
c++ 17已经引入optional了。
遇到个问题,我们需要一个函数能接受可能存在也可能不存在的参数。那么这时候可能使用absl::optional。但如果这个对象足够大以至于我们需要传递引用,那absl::optional就不好使了。
| |
如上所示,第一个选项可能无法满足您的要求。 如果有人将 Foo 传递给 MyFunc,Foo 将按值复制到 absl::optional<Foo>,然后将通过引用传递给函数。 如果您的目标是避免复制 Foo,那么您没有。
第二个选项会很棒,但不幸的是 absl::optional 不支持。(这篇文章2020-4-6更新的,不知道现在如何)
这时候,我们可以传递const *用nullptr代表不存在。
| |
这样和const Foo&传递一样高效,且支持空值。
std::optional 的文档指出,您可以使用 std::reference_wrapper 来解决不支持可选引用的事实:
| |
类似这样,但这太长且不易阅读,因此我们不推荐。
因此总结一下,如果您拥有可选的东西,则可以使用 absl::optional 。 例如,类成员和函数返回值通常适用于 absl::optional。 如果您不拥有可选的东西(即存在空的情况),只需使用指针,如上所述。
异常的问题,如果您的对象足够小以至于不需要通过引用,您可以将对象包装在 absl::optional 中,例如
| |
如果希望你的函数的所有调用者已经在 absl::optional 中有一个对象,那么你可以使用 const absl::optional&。 但是,这种情况很少见; 它通常仅在您的函数在您自己的文件/库中是私有的时才会发生。
tips 166: 什么时候 Copy is not a Copy
从c++ 17 开始,对象可能被原地创建。
| |
在 C++17 之前,上面拷贝或移动对象的次数最多为三个:每个 return 语句一个,初始化事物时还有一个。 这是有道理的:每个函数都可能将 BigExpensiveThing 放在不同的位置,因此可能需要移动以将值放在最终调用者想要的位置。 然而,在实践中,对象总是在变量 thing 中“就地”构建,不执行任何移动,并且 C++ 语言规则允许“省略”这些移动操作以促进这种优化。
在 C++17 中,保证此代码执行零复制或移动。 事实上,即使 BigExpensiveThing 不可移动,上面的代码也是有效的。 BigExpensiveThing::Make 中的构造函数调用直接构造了UseTheThing 中的局部变量thing。
编译器看到BigExpensiveThing()时,并不会立即创建临时变量。
相反,它将该表达式视为有关如何初始化某些最终对象的指令,但会尽可能长时间地推迟创建(正式地,“物化”)临时对象。
通常,对象的创建会延迟到对象被命名。 命名对象(上例中的 thing)使用通过评估初始化程序找到的指令直接初始化。 如果名称是引用,则将物化一个临时对象来保存该值。
因此,对象直接在正确的位置构造,而不是在其他地方构造然后复制。 这种行为有时被称为“保证复制省略”(guaranteed copy elision),但这是不准确的:一开始就没有副本。
简而言之,对象在首次命名之前不会被复制。通过值返回没有额外开销。
(并且根据tips 11,即使在给定名称之后,由于nrvo,局部变量在从函数返回时仍可能不会被复制)
那么有一个问题,什么时候未命名对象被拷贝呢?
在两种 corner case下,使用未命名对象无论如何都会导致副本
构造基类:在构造函数的基类初始值设定项列表中,即使从基类类型的未命名表达式构造时也会进行复制。 这是因为类在用作基类时可能会有一些不同的布局和表示(由于 virtual base classes和 vpointer 值),因此直接初始化基类可能不会导致正确的表示
| |
传递或返回小的平凡对象(trivial objects):当一个足够小的可平凡复制的对象被传递给函数或从函数返回时,它可能会在寄存器中传递,因此在传递之前和之后可能有不同的地址。
| |
还有一个细节,什么是 Value Category (值的范畴)
在C++中有两种表述。
产生值的那些词,例如 1 或 MakeAThing() - 您可能认为具有非引用类型的表达式。
那些产生一些现有对象的位置的词,例如 s 或 thing.data_[5] - 您可能认为具有引用类型的表达式。
这种划分为value category。前者是prvalue,后者是glvalue。我们前面所说的未命名对象即为prvalue.
所有纯右值表达式都在确定它们将值放在哪里的上下文中进行评估,并且纯右值表达式的执行用它的值初始化那个位置。
| |
prvalue 表达式 MakeAThing() 被评估为thing变量的初始化程序,因此 MakeAThing() 将直接初始化thing。 构造函数将指向thing的指针传递给 MakeAThing(),并且 MakeAThing() 中的 return 语句初始化指针指向的内容。
| |
相似的,编译器有一个指向要初始化的对象的指针,并通过调用 BigExpensiveThing 构造函数直接初始化该对象。
tips 186: 函数请放在匿名空间中
默认命名空间即全局命名空间,main()必须在该空间中。其他命名空间调用默认空间函数时,需使用 ::作用符号。
| |
当新加一个函数时,默认让它成为调用的.cc文件中的非成员函数。 如果有别的选择时,请放在匿名空间吧。
文章作者 clundro
上次更新 2022-06-28 (c45d85c)