More C++ Idioms C++ 惯常用法总结

总结一下以及可以用来做面试题的题目。最后总结以下所有的可以出题用于复习和面试出题。

内容来源:

The content below is a collection based on the source pages licensed in GNUFDL, All contents collected from the origin are redistributed in GNUFDL:

GNU Free Documentation License

C++ Idioms 条目分析:

  • Acyclic visitor 没搞明白
  • address of 是为了解决重载 operator & 的问题。
  • Attach by initializaiton,对于 event-loop 的程序以及接管 main 的程序(通常是 GUI 或者像 gtest 这种),有一些 main 之前的逻辑 。实现方案是用 static storage,如果有多个翻译单元的顺序要求,通过写 meyers singleton 然后写一个初始化函数依次调用他们。

 如果我需要在程序执行之前初始化一系列的对象,怎么初始化?

  • 律师 Attorney-client,因为 friend 可以访问所有 private 成员,解决方案是加一个中间人--律师

 如何控制 friend 类能够看到的成员?/如何对 friend 的视图做权限限制?

代码示例:

class Client {

private:

  void A(int a);

  void C(double c);

  friend class Attorney;

};

class Attorney {

private:

  static void callA(Client & c, int a) {

    c.A(a);

  }

  friend class Bar;

};

class Bar {

// Bar now has access to only Client::A and Client::B through the Attorney.

};

 

  • Barton-Nackman trick就是说,模板类写 friend 的 operator 比较,而不是写在 scope 外面。这样比较函数就不是模板重新另外决议的函数了,而是实例化之后的函数。一种 CRTP 模式 wrapper。

 operator 函数重载的时候用成员函数、friend 和在 namespace 中写有什么区别和优缺点?

代码示例:

template<typename T>

class EqualityComparable {

public:

    friend bool operator==(const T & lft, const T & rgt) {

            return lft.equalTo(rgt);

    }

    friend bool operator!=(const T & lft, const T & rgt) {

            return !lft.equalTo(rgt);

    }

};

class ValueType :

    private EqualityComparable<ValueType> {

 public:

    bool equalTo(const ValueType & other) const;

};

  • Base-from-Member如果需要在 base 构造之前使用 derived-class 的 member 的时候,一般的做法是通过 `nullptr` 和额外的一个 init 函数来实现。这里讲解了一个通过依赖于类的构造顺序,从而成功的把 has-a 变成了加一个 middleware 的 is-a。

 基类需要用到派生类的成员,怎么把派生类的成员传过去?/如何实现先初始化类的成员再初始化基类?

(其实这个是一个设计问题,尽量不要这样设计吧!但是有时候确实希望实现,比如非侵入式的修改,然后你又想用 has-a + inheritance 来封装一个类,比如下面的这个 fdostream。boost utility 还专门做了一个模板类)。

代码示例:

class fdoutbuf : public std::streambuf {

public:

    explicit fdoutbuf(int fd);

    //...

};

struct fdostream_pbase // A newly introduced class.{

    fdoutbuf sbuffer; // The member moved 'up' the hierarchy.

    explicit fdostream_pbase(int fd)

        : sbuffer(fd)

    {}

};

class fdostream

    : protected fdostream_pbase // This class will be initialized before the next one.

    , public std::ostream {

public:

    explicit fdostream(int fd)

        : fdostream_pbase(fd),   // Initialize the newly added base before std::ostream.

          std::ostream(&sbuffer) //  Now safe to pass the pointer.

    {}

    //...

};

  • Boost mutant=> BiMap 节约美德,对于一个 pair,如何避免复制的情况下实现 first 访问 second,second 访问 first 呢?简单,只需要进行一个 adapter 模式和暴力 POD `reinterpret_cast` 和引用/指针就行了。`boost::bimap` 的好处在于,避免复制的时候可以正向和反向同时查询,比如 hostname <-> ip address。

 如果我需要查询大量 key 和 value 的相互查询,怎么优化?

这种实现完全是编译开销,因为解释 first 和 second 只是不同的汇编解释。

代码示例:

template <class Pair>

struct Reverse

{

    typedef typename Pair::first_type  second_type;

    typedef typename Pair::second_type first_type;

    second_type second;

    first_type first;

};

template <class Pair>

Reverse<Pair> & mutate(Pair & p)

{

  return reinterpret_cast<Reverse<Pair> &>(p);

}

  • Calling Virtuals During Initialization首先复习几个问题,vptr 的填充是在 base ctor 之后, member init 之前进行的 [class.base.init]。因此构造函数中禁止了动态查找和虚函数绑定,此时只会调用自己的虚函数。解决方案即 two phase initialization ,一般结合 factory pattern 来做,不然 RAII 就无了,还是手写各种 init。法二是用 CRTP 不用虚函数,然后加限定域查找,但是太丑了而且 GP constraint 难以维护。

 你知道 vptr 是什么时候填充的吗?(先问: vtable 是什么时候初始化的?

 基类调用虚函数的时候调用的是谁的实现?如果在构造函数调用呢?如果在析构函数中调用呢?(这不是 UB)

 构造函数中可以对外暴露 this 指针吗?为什么不能。(废话)

  • Construction tracker利用逗号表达式(注意C++20 中下标内逗号不适用)实现构造成员捕获异常的 track。具体来说,就是构造的时候通过 `(tracker = TAG, (args...))`  作为参数去构造成员数据类型,从而可以知道是谁抛出的。或者另外起多个 bool 变量。

 构造的时候怎么知道构造哪个成员对象时抛出了异常?

补充一个我从来没用过的东西先:

 C++ 中构造 try catch block

class A{

...

public:

        A() try: m1(), m2()...{}

                catch(...){}

};

从而可以设计这样的代码:

... 

   A( TrackerType tracker = NONE)

   try    // A constructor try block.

     : b_((tracker = ONE, "hello")) // Can throw std::exception

     , c_((tracker = TWO, "world")) // Can throw std::exception

     {

        assert(tracker == TWO);

        // ... constructor body ...

     }

...

  • Copy-on-write这个 陈硕 在 muduo 书里面写过,Meyer 也提过,曾经的 gcc string 还用过。基本原理是用 const 访问的时候,就用原来的 shared_ptr, 如果需要用非 const 利用的时候,比如解引用或者调用了某些 setter ,利用 `shared_ptr` 的 `unique()` 接口判断(这个东西是不是安全的知乎还有一个问题讨论,SO 也有....)要不要复制一份。

 有一个很大的对象可能需要多个线程用到,大部分是读,少部分需要修改,怎么优化?

示例代码:

template <class T>

class CowPtr

{

    public:

        typedef std::shared_ptr<T> RefPtr;

    private:

        RefPtr m_sp;

        void detach()

        {

            T* tmp = m_sp.get();

            if( !( tmp == 0 || m_sp.unique() ) ) {

                m_sp = RefPtr( new T( *tmp ) );

            }

        }

    public:

      ... impl all copy/move stuffs...

};

  • EBO不说了,太简单了,对于可能的空类(最常见的是 Functor,用他基本是为了用他的 `operator()`),通过继承实现组合。一般 wrap 一个中间层,比如 stl 的 vector、map 等非 pmr allocator 的就是这样做的。但是 EBO 了之后,不同类的地址就可以一样,标准也没有保证多继承的时候地址可以区分。

 空类的大小是多少?如果要用到很多空类,怎么优化?

  • Erase-RemoveEffective STL I32 不知道这个为什么算一个 idiom... 大概是说,`std::remove` 只是 move,并不是 remove。因为他是通用的,他没有用 container 的引用参数,因此没办法修改 meta。所以一般要结合 `containter.erase` 来使用...

 怎么从容器类里面删除元素?(std::remove 怎么实现的?)/这个根本不算一个能问的问题...

  • Execute-Around Pointer: 通过指针和 `operator->`, `operator *` 的 overloading 封装一个 indirection 加载在对象访问的切面上,构造一个临时对象,因此也支持了 RAII。而且结合 cdr 和 con 模式,可以形成一个链条(继承)。

 AOP, Aspect Oriented Programming/面向切面编程

就是给所有的操作前后(运行步骤=切面)添加一些额外的重复脏活。Spring AOP 就是用这个,比如鉴权的时候。稍底层一点的用途比如每个点加 lockguard 和不加等。实际等于运行的时候调用的是代理的(Proxy),从而实现 python decorator 的效果。shared_ptr、unique_ptr 也是一种 AOP。

下面代码中,Aspect 做的只是封装了指针数据和 operator overloadings 而已:

template <class NextAspect, class Para>

struct Logging : Aspect<NextAspect, Para> {

  public:

    Logging (Para p)

        : Aspect <NextAspect, Para> (p)

    {

                std::cout << "Before Log aspect" << std::endl;

    }

    ~Logging ()

    {

        std::cout << "After Log aspect" << std::endl;

    }

};

 如果我要在每次对象的成员函数调用之间插入一些代码,怎么实现?

 如果我在每个函数返回的地方要加一些代码,怎么实现?

用 RAII。感觉这个点也没有什么问题好问的。我之前写过一个重载 | 运算符就是用的 AOP。

  • Expression-Template通过模板、Functor 实现表达式类,用于支持动态组装表达式, DSL、lazy evaluation。编译 parsing 中常用。

代码示例:

template < class L, class H, class OP >

struct DBinaryExpression {

  L l_;

  H h_;

  DBinaryExpression (L l, H h) : l_ (l), h_ (h) {}

  double operator () (double d) { return OP::apply (l_ (d), h_(d)); }

};

struct Add {

  static double apply (double l, double h) { return l + h; }

};

 怎么实现 lazy evaluation?

闭包、functor、函数指针

 如果我要运行时决定运算的表达式,运行时修改程序代码,怎么做?

写一个 jvm,写一个 lua vm .... 写 AST、用这个 expression-template

  • Inline Guard Macro: 这是其中一个原因为什么一些源码里面的关键字不是直接用的,而是用用宏加的,还有一些变量定义、返回值和返回等都是用宏包的一个,基本就是为了编译器的兼容和方便控制 release 和非 release 版本。

 ipp 文件的用途

一般 ipp 文件是基于头文件或者实现 include 再抽一部分出来,boost 里面有大量的 ipp 文件,主要是用来抽开 implementation 和干净的头文件以及用于链接编译的非 inline 的 impl。

这里主要讲解 inline 的既能避免不 inline 的 redefinition,又能避免 inline 了必须放头文件的用法:

// test.ipp file

INLINE void Test::func(){}

/*******************************************************************/

// test.hpp file#define MYPROJECT_TEST_H

class Test { public: void func();};

#ifdef MYPROJECT_INLINE_ENABLED

#define INLINE inline

#include "test.ipp"  

#endif

/*******************************************************************/

//test.cpp file

#include "test.hpp"

#ifndef MYPROJECT_INLINE_ENABLED

#define INLINE

#include "test.ipp"

#endif

  • Inner Class: 对于 string 这种不能继承的类,可以通过继承变成组合 + static cast operator 来实现,当然实际表达能力肯定是比直接继承或者 go 的 embedded 差一点的。

不放代码了,太长了....

  • Int-To-Type: 就是 tmp 里面把同一个类型的数值变成不同类型的...比如 integral_constant 。这个不具体说了。

 模板编程里面怎么让 1 和 2 变成不同的类型?

  • Interface 纯虚函数有意义吗?我认为 interface 的最佳实现应该是 GP + constraint(concept),比如 golang 里面的那个 interface (的写法),但是 go 的实现是用 hashtable 来做的虚表我不理解...不过他也没有静态模板,要运行时决定使用虚表也理所当然了(但是这个虚表好像是运行时造的,因为他并没有指定一个 `implements` 语法, 因此必须根据反射信息来建立表或者查询)。相似的还有 Rust 中的 trait 和 fat pointer (效率比原 pointer 高是因为 fat pointer 等于一层值语义的 wapper 实现的指针语义,不用再走一趟才知道 vtable 的地址,而且对象数据存储也不用偷偷存一个和行为有关的数据了)。类似 rust 的两个指针的实现提案 p0957 从语法层面修订到现在的库层面(revision 7)搞了一个 `std::proxy` 的 wrapper,可能有望 C++26 进入 std。p0957 的思想等于是不把 `多态` 这一语义绑定到类本身,因为多态某种程度上可以认为是用法上的。具体这里不再深究。

 Polymorphism is not a property of the type, but rather a property of how it is used.

 Inheritance Is The Base Class of Evil (Adobe  Sean Parent 2003)

 Better Code: Runtime Polymorphism - Sean Parent (still Adobe Sean Parent NDC 2017)

  • making-new-friend: 在类里面定义 friend 函数,避免特化影响决议... 因为 friend 函数实际是 static 的。或者说,就是 friend 函数不应该是一个模板,而是一个实例化出来的有特定类型信息的函数。

代码示例:

template<typename T>

class Foo {

   T value;

public:

   Foo(const T& t) { value = t; }

   friend ostream& operator<<(ostream&, const Foo<T>&);

};

template<typename T>

ostream& operator<<(ostream& os, const Foo<T>& b) {

   return os << b.value;

}

上面这种写法无法编译,因为这个函数不是一个模板函数,除非 friend 中加上 <> 限定。因此不如直接 inner 定义。不过如果内部定义的话不加限定只能 ADL 不能参与非 ADL: [namespace.memdef]/3。

 ADL: argument dependent lookup

意义是如果参数已经加了限定符,函数调用就不用限定符,这样对于 operator 来说是很重要的,不如代码就会很丑。ADL 的优先级超越 using,例如以下的例子:

    using std::swap;
    swap(obj1, obj2);

这里 `obj1` , `obj2` 可以是通过 `A::obj1` 这种方式引入的。如果非模板冲突的时候(能编译的时候说明没有实例化模板,不能编译说明存在重载),还会引发 ambiguous。

如果没有 ADL,每次调用都需要加限定符 `X::`。

具体到这里,friend 如果定义在内部,而且参数和类型无关,则无法调用。比如以下的代码:

struct S {

    friend void f() {}

    friend void g(S const&) {}

} const s;

int main() {

    // f();     // error: 'f' was not declared in this scope

    // S::f();  // error: 'f' is not a member of 'S'

    g(s);

    // S::g(s); // error: 'g' is not a member of 'S'

}

 friend 函数是不是破坏了封装吗?

要看你怎么看待封装,如果把封装的谈论对象限定在单个类里面,是的。但是 OOP 不等于面向单一类编程。另外一个层面是 C++ 不是纯粹的 OO 语言(BS 说封装都不是 C++ 的属于),另外,Java 中的 get set 本身也是一个曲线救国,这个角度来看 friend 比 get set 更加的安全。

  • Multi-statement Macro:为了让 macro 后面可以且必须加分号,通过一个 `else` 或者 `while(0)`来实现。
  • Member Detector: compile time reflection 和 type_trait 但是还是要很麻烦的。这里通过 SFINAE 实现。基本原理就是让他的成员函数有多个模板重载,然后如果有这个成员,就会重载成功。具体的不说了,巧妙利用 decl type 和修改参数类型就行了。
  • Named Constructor:通过 `static return {}` 来构造对象,提高可读性。就像 make_shared 这些。
  • Named loop用 macro magic 来实现 `break(tag)` 的语法。

直接看代码把家人们:

#define named(blockname) goto blockname; \

                         blockname##_skip: if (0) \

                         blockname:

#define break(blockname) goto blockname##_skip

int main(void) {

  named (outer)

  for (int i = 0; i < 10; i++) {

    int j = 0;

    named(inner)

    for (; j < 5; j++) {

      if (j == 1) break(outer);

      if (j == 3) break(inner);

    }

    std::cout << "after inner\n";

  }

  return 0;

}

原理就是这样的原理... 不说了。Java 和 go 都支持 label break 了。

SO 上面有一个更安全写法的讨论:

  • Named Parameter:为了让默认参数不局限于吊车尾,通过一种 placeholder + setter 来做变量的初始化,前提是所有的 member 都有 default,没有的话就丢到真的构造里面,有 default 的通过 setter 来搞。看代码吧家人们。boost 里面的图论算法用到了。另外对于参数太多的函数(超绝参数化了属于是,这种情况如果走构造函数一般要上栈传参了)也能用上。

代码示例:

int main (void)

{

  // The following code uses the named parameter idiom.

  std::cout << X::create().setA(10).setB('Z') << std::endl;

}

  • Nifty counter, 这个东西是用来保证用到了才初始化 singleton 的(本质上等于 singleton 模式)。这里有挺多点需要掌握的。下面一个一个来分析。

 dynamic library 中的变量

shared 库的所有变量对于多个程序而已都是独立的,因为他们的地址空间是独立的,shared library 只会 share  一个东西,就是代码段。因此我们的讨论主要是讨论 translation unit 的独立。

 C++ 中的 static

  • *.c 中的 static(区别于默认的 extern),实际可以用匿名 namespace 来实现。
  • class 中的  static,实际等于 global 的 extern,只不过访问需要加上 class 的限定符。
  • 有一个问题是,能不能让 class 的 static 具备区别于 extern 的 static ?答案是不能,因为 class 的 member 定义才能加 static,而定义是在 header 里面的。
  • 函数中的 static(local static),即 C++11 以后支持的 singleton 语法糖(static member function defined in header and static variable in function),这个东西是每个翻译单元都共享的,因为如果你定义 static 函数在头文件里面,这个东西会违反 ODR,因此他是默认 inline 的,如果你不 inline,那么就要 link,所以也是 shared 的,除非你每个翻译单元静态链接一个,最后链接的时候就会报错 multiple defs。
  • 所以一般如果共享库需要独立的每个翻译单元分开避免串味,一般要采用 plain static 变量配送 setter getter。

 static 变量的构造时机

(1): The correct state should be: "before any function from the same translation unit is called". 

来自 < C++ - Constructor call of global static object and local static object is different? - Stack Overflow>

  • 全局和静态成员:main 之前
  • 本地静态:第一次执行到达
  • 本地静态 POD:main 之前

如果对 meyer's singleton 进行 cache 的操作,会引发问题,因为等于你还是要做一套判断。因此最好不要做全局 cache,可以在用到他的对象身上加一个 reference cache。比如下面的情况中,由于 cache 分散在不同的编译单元:

// user2.cpp -> libu2.so / u2.o

#include "shared_singleton.hpp"

my_class &cache2 = singleton<my_class>::Instance();

// user1.cpp -> libu1.so / u1.o

#include "shared_singleton.hpp"

my_class &cache1 = singleton<my_class>::Instance();

// main.cpp -> a.out

extern my_class cache1;

extern my_class cache2;

int main() {

  using namespace std;

  cout << "cache1 addr: " << &cache1 << endl;

  cout << "cache2 addr: " << &cache2 << endl;

}

此时由于 extern 进来的两个变量实际来自两个翻译单元,无论他们以静态链接还是动态链接,他们都是两个独立的 singleton。下面加一部分怎么确认 local static singleton 是独立的符号的点,通过大纲视图折叠。

因为编译器保证了两个引用变量在程序执行之前就初始化。上面的文件实现,我们可以通过以下命令查看符号属性(nm 命令是解析 object 文件的):

nm -a libsu1.so | grep t | xargs c++filt | grep -5 Instance

我们可以看到:

W

singleton<my_class>::Instance()

0000000000004028

u

singleton<my_class>::Instance()::t

u 表示这是一个 unique 符号。

 单例模式在不同翻译单元中是同一个吗?

 全局变量什么时候初始化?全局变量的初始化顺序是怎么样的?析构呢?

同一个翻译单元中,按照定义的顺序在任何函数执行之前初始化。不同翻译单元不知。析构顺序和初始化顺序相反。对于编译期可以知道的变量(一般是字面常量、简单的函数初始化等),一般会在 load 的时候,执行之前就执行完毕。

 如果我需要在程序执行之前执行一段程序,怎么实现?

通过全局变量的声明、定义来实现,即 RAII 利用构造函数实现或者利用变量初始化 lambda。

  • NVI-Non-virtual-Interface/Protected VirtualHerb Sutter 也推动 p0957,持续搞模板,看来他也不喜欢继承类似子类沙箱模式,方便注入 decorator 逻辑。本质上等于 framework 完形填空。当然问题也很明显就是每一个成员都要写两次。但是这其实也不是很糟糕的事情如果对象参数和成员方法太多,我们总是可以进行分离的,至少 Single responsibility 还是可以用一用的。
  • Object Generator 即各种 make_xx, bind_xx。和工厂方法的区别就是,工厂是用来实现多态的,object generator 是用来减少编程负担的(比如十几行的 type trait 加 typedef...)...
  • Parameterized base class: 这个和 CRTP 的不同是 CRTP 是让父类获取子类信息,PBC 是用继承来实现组合的语义,或者说模块化。
  • Policy Clone/policy rebindAllocator 的 rebind 实现思路。这是因为 C++ 中,`Policy<_Th1>` 可以匹配模板 `template <typename T>`中的 `T`,因为他本身就是一个类型,除非我们用 `template<typename <class _Th>P>` 去匹配,这样可以匹配到 `P = Policy && _Th = _Th1` 但是这样写的话灵活性不好。因此一般通过让 Policy 自己支持 rebind。具体代码也不放了。我认为这个绑定 allocator 和分配类型的设计就十分混乱...

 STL 的 allocator 是怎么让不同类型的也能分配内存的?比如我给 map<int> 传一个 std::allocator<int>, 分配节点的内存的时候怎么能通过 int allocator 分配内存呢?

STL 中这一点很奇怪,因为他自己都会用 rebind,为什么不能对用户的参数直接做 rebind 呢?可能大概是为了零开销以及模板的功能性的历史原因。节点的 rebind 是必要的,而用户传进来如果真的需要,用户应该自己 rebind。

  • Requiring or Prohibiting Heap-based Objects:通过一些手段禁止堆分配以及只允许堆分配。最简单的方案是,protect  析构函数->只允许堆分配,protect static 的 operator new 的 override -> 只允许栈分配或者类自己管理的堆分配。

 怎么限定一个类只能堆上创建?怎么限定一个类只能栈上创建?

  • Return Type Resolver:利用类的构造函数伪造成函数,结合 operator T (static_cast)来实现函数返回值的推导,从而生成他们要的东西。

直接看代码吧家人们:

class getRandomN

{

  size_t count;

public:

  getRandomN(int n = 1) : count(n) {}

  template <class Container>

  operator Container () {

    Container c;

    for(size_t i = 0;i < count; ++i)

      c.insert(c.end(), rand());

// push_back is not supported by all standard containers.

    return c;

  }

};

int main()

{

  std::set<int> random_s = getRandomN(10);

  std::vector<int> random_v = getRandomN(10);

  std::list<int> random_l = getRandomN(10);

}

 type a = f(),  怎么让 f 支持不同的 type ?(这个不能算一个问题吧...)

  • Safe bool: 写 operator bool 的时候一定要加 explicit (除非你知道你在做什么),否则会引发问题:if(a==b) 其中 a b 对象只支持了 operator bool() 因此,很可能得到错误的结果。

 让对象支持 bool 判断有什么值得注意的点吗?

答案是不要让对象支持 bool 判断。

  • Scope Guard:这个我在做 15-445 的时候就搞了很多,结果是反而搞复杂了... 等于如果你要让  throw 之后保证资源释放,那就做一个 RAII guard,但是如果正确执行过程中,资源又有可能释放,所以要加一个 release。只能说是为了保障异常安全吧。如果正常路径一般不需要释放的话,那倒不如在 catch 中做,然而问题是 throw 是我们 throw 的,我们不 catch。还有一个方法就是我们可以 throw 之前自己把资源处理完,但是我们可能在很多个地方 throw.... 看着办吧。
  • shrink to fitC++ 11 之后让 vector 等容器(顺序)支援了这个,避免了内存泄漏(monotonic memory resource 了属于是)。
  • small object/buffer optimization: 不说了,一般来说就是优化到你的 control block 的大小,两个机器指针大小之类的?
  • Thin Template:提取所有不依赖于模板类型的独立一个基类出来,避免模板膨胀太多。

 模板在文件中是每个类型都生成一份吗?这样如何减少代码段大小开销?

支持 LTO 的编译器会在链接时对同一个类型的进行 weak symbol 的合并,不同类型的都会生成一份独立的,采用  thin template idiom 减少代码膨胀。

  • Type erasure也就那样。基本思路就是用类似虚函数/函数指针,然后基类指针。具体我不说了。

 你知道 std::function 是怎么实现的吗?

简简单单报个菜名,通用函数指针(lambda、operator() 等都可以用函数指针的语义实现,即,解引用,然后调用,这个可以模板化为一个函数),可变模板参数,指针,动态内存分配(比如用于复制 stateful 的 object operator()、lambda object 等)+virutal clone。

  • Type selection用模板元编程来决定特化生成的类的成员类型(用 std::conditional),比如是用 long 还是用 int,std::bitset 可以用, 只是要求参数是 constexpr 而已。
  • Vitual Ctor/virtual clone神奇的一点就是,虽然 ctor 不能是 virtual,但是我们可以用工厂模式的来实现这种需求。比较重要的用途在于多态场景下的 virtual clone ,当然我们肯定不需要 virtual copy ctor 因为... 你不会用 *(obj1) = *(obj2) 这种做法来写代码,which is error-prone + 难读的。

 构造函数可以为虚吗?如果我要根据父类指针复制对象,怎么实现?

那当然是学 Java 实现一个虚的 clone 方法啦!

  • virtual friend一样的套路,让 friend 函数调用一个虚函数实现,而不是直接写逻辑。(如果一个中间层不够,就再加几层!)
  • capability query:用 dynamic cast 检查 interface 设计的多继承。但是 dynamic cast 需要开启 rtti,而且没有虚函数的用不了。并且,RTTI 也是同样的需要至少一个虚函数。不过,这个限制就是当你把 dtor 写成 virtual 的时候就自动满足了。对于一些情况,与其走这个虚表查询,不如你自己做一个 virtual flag 在基类.... OOP 罪恶。可以用 double dispatch 避免 instanceof/dynamic_cast  (但是 GoF 中用双分派加强耦合是因为当时 C++11 没出,没有 RTTI)。我认为不如用一个 virtual flag/enum 了。但是 dynamic_cast 又是必不可少的,只要用到 interface based polymorphism(即多继承体系,以及连续继承等),而 type flag/virtual falg 只支持类似于 abstract class、单一 interface 多个不同子类等,即超绝耦合,父类需要知道子类的信息(当然,用一种额外的 id 的话确实可以不修改父类,总之方法有很多啦)。

 你知道 dynamic_cast 的实现原理吗?

我们并不知道实际是怎么实现的除非我们看了源码,但是我们确实有实现的思路。首先编译器完全知道继承的体系结构,因此只要知道了 most derived class,就能知道一路上的所有类型,因此 type_info 只需要有 most derived class,就足够了,剩余的信息编译期就能生成。

visitor 模式示例代码,但是无 instanceof visitor 模式最大的问题是他耦合太强了。但是,其实并没有耦合,因为我们

可以重载 accept 函数

// 客户端代码

foreach (Node node in graph)

    node.accept(exportVisitor)

// 城市

class City is

    method accept(Visitor v) is

        v.do(this)

    // ...

// 工业区

class Industry is

    method accept(Visitor v) is

        v.do(this)

// ...

  • checked deletedelete 必须要看得到析构函数,in-complete type 上调用 delete 是 UB(最根本是他甚至不知道 dtor 是不是 public 的,可不能 ub 吗)。

 delete 有什么值得注意的地方吗?

有的,一个比较常见的八股文是说数组,因为有些编译器实现是在 malloc 出来的内存地方放一个数组尺寸,而且如果 delete 默认支持数组,就不再是零开销抽象了。第二个就是这个 UB。

  • Coercion by member templateC++ 默认支持子类到父类指针转换,但是不支持模板的。则是因为如果模板不是多态语义使用模板参数类型的话,是实现不了的。如果确实是指针语义,一般都要支持这个,自己写一些运算符重载和 copy assign 的东西。比如智能指针肯定要支持啊(虽然 unique 只能支持 move)。。。

motivation 示例代码,具体实现就不说了,就是写成员函数:

class B {};

class D : public B {};

template <class T>

class Helper {};

B *bptr;

D *dptr;

bptr = dptr; // OK; permitted by C++

Helper<B> hb;

Helper<D> hd;

hb = hd; // Not allowed but could be very useful

总结以下:

引用

 Polymorphism is not a property of the type, but rather a property of how it is used.

 static 变量的构造时机

知识点

 ADL: argument dependent lookup

 AOP, Aspect Oriented Programming/面向切面编程

 C++ 中的 static

 C++ 中构造 try catch block

 dynamic library 中的变量

 ipp 文件的用途

 你知道 dynamic_cast 的实现原理吗?

问题

 delete 有什么值得注意的地方吗?

 friend 函数是不是破坏了封装吗?

 operator 函数重载的时候用成员函数、friend 和在 namespace 中写有什么区别和优缺点?

 STL 的 allocator 是怎么让不同类型的也能分配内存的?比如我给 map<int> 传一个 std::allocator<int>, 分配节点的内存的时候怎么能通过 int allocator 分配内存呢?

 type a = f(),  怎么让 f 支持不同的 type ?(这个不能算一个问题吧...)

 单例模式在不同翻译单元中是同一个吗?

 构造的时候怎么知道构造哪个成员对象时抛出了异常?

 构造函数可以为虚吗?如果我要根据父类指针复制对象,怎么实现?

 构造函数中可以对外暴露 this 指针吗?为什么不能。(废话)

 基类调用虚函数的时候调用的是谁的实现?如果在构造函数调用呢?如果在析构函数中调用呢?(这不是 UB)

 基类需要用到派生类的成员,怎么把派生类的成员传过去?/如何实现先初始化类的成员再初始化基类?

 空类的大小是多少?如果要用到很多空类,怎么优化?

 模板编程里面怎么让 1 和 2 变成不同的类型?

 模板在文件中是每个类型都生成一份吗?这样如何减少代码段大小开销?

 你知道 dynamic_cast 的实现原理吗?

 你知道 std::function 是怎么实现的吗?

 你知道 vptr 是什么时候填充的吗?(先问: vtable 是什么时候初始化的?

 全局变量什么时候初始化?全局变量的初始化顺序是怎么样的?析构呢?

 让对象支持 bool 判断有什么值得注意的点吗?

 如果我需要查询大量 key 和 value 的相互查询,怎么优化?

 如果我需要在程序执行之前初始化一系列的对象,怎么初始化?

 如果我需要在程序执行之前执行一段程序,怎么实现?

 如果我要运行时决定运算的表达式,运行时修改程序代码,怎么做?

 如果我要在每次对象的成员函数调用之间插入一些代码,怎么实现?

 如果我在每个函数返回的地方要加一些代码,怎么实现?

 如何控制 friend 类能够看到的成员?/如何对 friend 的视图做权限限制?

 有一个很大的对象可能需要多个线程用到,大部分是读,少部分需要修改,怎么优化?

 怎么从容器类里面删除元素?(std::remove 怎么实现的?)/这个根本不算一个能问的问题...

 怎么实现 lazy evaluation?

 怎么限定一个类只能堆上创建?怎么限定一个类只能栈上创建?

要访问的网站

  More C++ Idioms - Wikibooks, open books for an open world

 Better Code: Runtime Polymorphism - Sean Parent (still Adobe Sean Parent NDC 2017)

 c++ - How is std::function implemented? - Stack Overflow

 https://en.wikibooks.org/wiki/More_C%2B%2B_Idioms/Nifty_Counter

 Break-able named scopes in C/C++ - Stack Overflow

 Inheritance Is The Base Class of Evil (Adobe  Sean Parent 2003)

 More C++ Idioms/Inner Class - Wikibooks, open books for an open world

 

暂时标记为完结 2022/5/28 15:56

This page is generated by OneNote.

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值