您从经验中学到了哪些与 C++ 相关的习语、误解和陷阱?

Posted

技术标签:

【中文标题】您从经验中学到了哪些与 C++ 相关的习语、误解和陷阱?【英文标题】:What are some C++ related idioms, misconceptions, and gotchas that you've learnt from experience? 【发布时间】:2010-09-22 13:58:20 【问题描述】:

一个例子:

class A

  public: 
  char s[1024];
  char *p;

  A::A()
  
    p = s;
  

  void changeS() const
  
    p[0] = 'a';
  

;

即使知道changeS是一个const成员函数,它是在改变对象的值。所以一个 const 成员函数只是意味着它将所有变量都视为 const,并不意味着它实际上会保留所有成员 const。 (为什么?成员函数上的 const 关键字将 char *p; 视为 char * const p; 而不是 const char *p;

这意味着 p 不能指向其他东西。并不是说你不能改变 p 的数据。

【问题讨论】:

这不应该是一个社区维基吗? 【参考方案1】:

自从我在一些代码中发现它以来,我就喜欢它了:

assert(condition || !"Something has gone wrong!");

或者如果你手头没有条件,你可以这样做

assert(!"Something has gone wrong!");

以下归属于@Josh(见cmets)。它使用 comma operator 代替:

assert(("Something has gone wrong!", condition)); 

【讨论】:

为什么要双重否定? (assert(c && "expected c")) 我用逗号:assert(("Something has go wrong!", condition)); 可爱,也喜欢乔希和雨果的方式。【参考方案2】:

通常会绊倒人们的一些事情:

std::cout << a << a++ << --a;
i = ++i;

以上行均未定义。

void foo(bar* b1, bar* b2);

int main() 
  foo(shared_ptr<bar>(new bar()), shared_ptr<bar>(new bar()));

以上可能会泄漏内存。

int* arr = new int[10];
arr + 11;

这会导致未定义的行为。

至于成语,我最喜欢的是RAII。在栈上分配对象,保证在对象超出范围时调用析构函数,防止资源泄漏。

【讨论】:

你需要解释为什么对 foo() 的调用可能会泄漏。这对外行来说并不明显。 你确定 arr+11 会导致未定义的行为吗?我的印象是,这只发生在您尝试取消引用非法指针时。 Martin:想象一下这个执行顺序:new bar()、new bar()、shared_ptr()、shared_ptr()。如果在第二个 bar 对象的构造过程中有一个接受,则不会释放第一个 bar 对象。 Martin:是的,即使创建指针也是未定义的行为,无论它是否被取消引用。我不确定它是否会在任何地方产生实际影响,但我怀疑这是为了避免意外溢出。 要明确:arr + 10 定义明确且合法,但不能取消引用; arr + 11 未定义。【参考方案3】:

您无需了解 C++ 复杂的函数 typedef 声明语法。这是我发现的一个可爱的技巧。

快速,描述一下这个 typedef:

typedef C &(__cdecl C::* const CB )(const C &) const;

简单! CB 是指向 C 类成员函数的指针,它接受对 C 对象的 const 引用并返回对 C 对象的非常量引用。哦,它是一个 const 成员函数。哦,函数指针本身是 const…(对吗?)

C++ 函数声明规范语法是出了名的晦涩难记。是的,经验丰富的 C++ 资深人士可能会使用一些技巧来破译这种恐怖,但这不是本技巧的内容。这个技巧是关于你如何不需要记住这种可怕的语法并且仍然能够声明这样的函数指针类型定义(例如,如果你正在与一些从未听说过 boost::function 的遗留 API 交互)。 让编译器为您完成工作,而不是费尽心思。下次您尝试为成员函数创建 typedef 时,如下所示:

struct C 
        const C& Callback(const C&) const    
;

与其费力手动想出上面的复杂语法,不如引发一个故意的编译错误,这将迫使编译器命名野兽。

例如:

char c = &C::Callback;

编译器高兴地吐出这个有用的错误消息:

“… cannot convert from 'const C &(__cdecl C::* )(const C &) const' to 'char'”

这是我们正在寻找的。 :)

【讨论】:

IIANM,__cdecl 实际上不是 C++。【参考方案4】:

当我们不知道以后是否需要时,不要浪费时间尝试在类上实现复制操作。我们处理的许多对象只是实体,复制它们几乎没有任何意义。使它们不可复制,如果确实需要,稍后再实施复制/复制。

【讨论】:

uncopyable 类继承(私有),如 Scott Meyers 在“Effective C++”,第 3 版中所建议的那样。 - 第 6 项是一个很好、简单的方法。 从 boost::noncopyable 私有继承。 如果你早点而不是晚点实现复制,你最终只会在更新课程时不断记住修改复制代码! 为了完整起见,请提及实现不可复制性的技术(例如,将复制 ctor 声明为私有而不实施)。【参考方案5】:

一个很少使用但很方便的 C++ 习惯用法是在构造函数链中使用 ?: 运算符。

class Sample
  
    const char * ptr;
    const bool  freeable;

    Sample(const char * optional):
        ptr( optional ? optional : new char [32]),
        freeable( optional ? false : true ) 
    ~Sample( )   if (freeable) delete[] ptr; 
  

C++ 不允许在构造函数的主体内更改 const 值,因此这避免了 const-casts。

【讨论】:

您的班级似乎拥有该指针。在这种情况下,您可能应该使用unique_ptr 并使用unique_ptrowner【参考方案6】:

如果您有一个没有值语义的类,请确保明确声明以下所有构造,以防止出现问题。

默认构造函数 复制构造函数 赋值运算符

在许多情况下,您只需要声明这些结构的一个子集。然而,在某些情况下,哪些需要哪些不需要,这可能会变得非常棘手。将所有 3 项声明为私有并处理此事要安全得多。

在顶部添加注释说明这不是复制安全类也很有帮助。

为您节省时间。

【讨论】:

"address of" 运算符怎么样,它是 c++ 运算符的锥形集合的最后一部分。 \& 运算符没有上述运算符/函数所具有的问题效果。它们会导致出现非复制安全数据的隐式副本。 \& 操作符只是返回一个地址到这个数据本身是安全的。某人对该地址所做的事情可能是邪恶的。【参考方案7】:

这是我有一天抓到的另一只:

char int2hex(int x) 
     return "-0123456789abcdef"[(x >= 0 && x < 16) ? (x + 1) : 0];

它只是索引 char 数组而不是进行切换。如果超出范围,则返回'-'。

【讨论】:

我会在最后加上“-”并以 ... 结尾? x : 16]; 最初他们只是做了“0123456789abcdef”[x]。我不想让人们认为我鼓励缓冲区溢出:p 为什么不写 '((x >= 0 && x 【参考方案8】:

因为我们都忽略了 OP,而是发布了我们最喜欢的酷把戏......

使用 boost(或 tr1)shared_ptr 在运行时保持类不变(有点明显,但我没有看到其他人这样做):

#include <cassert>
#include <functional>
#include <stdexcept>
#include <boost/shared_ptr.hpp>
using namespace std;
using namespace boost;

class Foo

public:
    Foo() : even(0)
    
        // Check on start up...
        Invariant();
    

    void BrokenFunc()
    
        // ...and on exit from public non-const member functions.
        // Any more is wasteful.
        shared_ptr<Foo> checker(this, mem_fun(&Foo::Invariant));

        even += 1;
        throw runtime_error("didn't expect this!");
        even += 1;
    

private:
    void Invariant()  assert(even % 2 == 0); 
    int even;
;

【讨论】:

这是立即终止程序的好方法。在另一个异常处于活动状态时抛出一个异常将执行 terminate()。 有趣。我想我会在 ctor 和 dtor 中声明一个名为“Invariant”的朋友子类,并在每个公共成员函数的开头在堆栈上声明它的一个实例。您的代码稍微短一些,但它只在函数退出时检查。 这是故意的。您不需要检查入口,因为不变量保留在最后一个出口。 我明白了。好吧,您可能想查看 Loki 的 ScopeGuard,它的功能大致相同,但没有 shared_ptr 完成的动态内存分配:您只需调用“ON_BLOCK_EXIT(&Foo::Invariant);”链接:loki-lib.sourceforge.net/index.php?n=Idioms.ScopeGuardPointer @j_random_hacker:自 Loki 时代以来,Andrei Alexandrescu 的 ScopeGuard 已经更新......在CppCon 2015 中给出了很好的介绍,还有一个proposition to put it into the standard library,带有代码。【参考方案9】:

我不能说我是一位经验丰富的 C++ 程序员,但我最近了解到将数组和数组作为函数参数传递是多么困难。尽量避免这种情况:(

如果您在编译时知道大小,则很简单。即使您在编译时知道其中一个维度。 如果你根本不知道......你可能正在看类似这样的东西

m[i*dim2+j]

i 是行的迭代器,dim2 是列数,j 是列的迭代器。

【讨论】:

您必须知道除了其中一种尺寸之外的所有尺寸...无论如何,请使用 std::vector。我们在这里讨论的是 C++,而不是 C。【参考方案10】:

自从我了解了 RAII(有史以来最糟糕的首字母缩略词之一)和智能指针,内存和资源泄漏几乎完全消失了。

【讨论】:

我在一些 irc 频道中听到的另一个名称是 SBRM(范围绑定资源管理),这是一个更好的名称。【参考方案11】:

您通常可以在源文件中隐藏比您想象的更多的东西。如果没有必要,不要将所有内容都设为私有 - 通常最好将其保留在源文件中的匿名命名空间中。我发现它实际上使事情变得更容易处理,因为这样您就不会透露实现细节,而是会受到启发来制作许多微小的功能而不是单一的功能。

【讨论】:

【参考方案12】:

有时,标头会因不具有类似宏名称的行为而受到污染

#define max(a, b) (a > b ? a : b)

这将使使用 max 函数或以这种方式调用的函数对象的代码无效。一个臭名昭著的例子是windows.h,它就是这样做的。一种解决方法是在调用周围加上括号,这会阻止它使用宏并使其使用真正的 max 函数:

void myfunction() 
    ....
    (max)(c, d);

现在,最大值在括号中,不再算作对宏的调用!

【讨论】:

该死。 (为什么 windows.h 还需要定义这个呢?)【参考方案13】:

我在编程时实际上没有遇到任何问题,但一位朋友想要解释为什么代码可以工作。我花了一段时间才弄清楚。也许对你们来说很明显,但我不是一个经验丰富的程序员。

#include <iostream>
using namespace std;

int& strangeFunction(int& x)return x;


int main()
        int a=0;
        strangeFunction(a) = 5;               //<------- I found this very confusing
        cout << a <<endl;
        return 0;

【讨论】:

在 C++ 中很常见,并且是许多标准模板库类的常用习语。 int& 现在被称为“左值引用”,“左值”一词的原意是它可以出现在赋值的左侧。按照约定,5 是一个右值,因此只能出现在赋值的右侧(更准确的定义是它没有地址)。【参考方案14】:

除非需要,否则不要使用shared_ptr。更喜欢使用 C++ 引用和unique_ptrshared_ptr 是一个性能狂,使代码看起来比实际更复杂。通常这不会是一个问题,除了shared_ptr 的不寻常的吸引力和它的传染性。

【讨论】:

【参考方案15】:

除非需要,否则不要陷入使用std::noncopyable 的陷阱。是的,它在很多地方都很有用,应该在那里使用。

陷阱是人们开始编写clone() 函数并使其不可复制,这实现了相同的功能。相反,您也可以将explicit (link) 用于复制构造函数,以防止意外复制(并将赋值设为私有,或删除 C++0x 中的函数)。不过,继承的基类需要clone()

【讨论】:

【参考方案16】:

使用boost::spirit::hold_any (link) 代替boost::any 用于性能代码(同时持有大量小对象)。我看到他们的表现有很大的不同。

【讨论】:

以上是关于您从经验中学到了哪些与 C++ 相关的习语、误解和陷阱?的主要内容,如果未能解决你的问题,请参考以下文章

Java中是不是存在用于满足接口的空方法的习语?

在字典中添加新列表或附加到列表(如果存在)是不是有一个很好的习语?

函数式编程:将列表划分为给定大小的较小列表的习语?

C++ 程序员应该使用哪些 C++ 习语? [关闭]

C++“命名参数习语”与 Boost::Parameter 库

我从两年的JavaScript函数式编程中学到了什么?