C++ 要点摘录

28 May 2021 | notes, cpp

里面有一些存疑的地方

模板基类Policy的析构函数

c++设计新思维 1.7 Policy Class的析构函数

基类指针指向派生类实例,并delete该指针。如:

typedef WidgetManager<PrototypeCreator>
    MyWidgetManager;
...
MyWidgetManager wm;
PrototypeCreator<Widget>* pCreator = &wm;
delete pCreator;

解决方法:定义non-virtual protected析构函数。只有派生类才可以delete这个指针

protected 或 private析构函数

从语法上来讲,一个函数被声明为protected或者private,那么这个函数就不能从外部直接被调用了。protected的函数,可以被子类的内部函数调用。private的函数,只能被本类内部函数调用。

因此,protected析构函数可用于禁止直接实例化该类,不影响其子类;限制栈对象生成。

析构函数声明为private,该类不能被继承。

为什么c++没有反射

c++设计新思维 8.2 Object Factories in C++: Classes和Object

在C++中classes和object是不同的东西。classes由程序员产生,objects由程序产生。无法在执行期产生新的classes,也无法在编译期产生objects。你无法拷贝一个class,将它保存于变量中,或是从某个函数中返回。

但是在某些语言中,classes就是objects。在那些语言中,具有某些属性的objects习惯上会被视为classes。因此,在那些语言中,你可以于执行期产生新的classes,拷贝一个classes,将它保存于变量等等。

friend

友元保护了封装性。如果没有friend你就得把需要访问的元素设成public了

主要用于主类+辅助类这种设计方式。辅助类需要可以访问主类的私有成员。在这里辅助类实际逻辑上是主类的一部分,但是物理上分开更好,常见的辅助类比如迭代器,守护器等等。

friend operator大多数情况下就是起着成员函数的作用,只不过因为操作数顺序问题没搞成成员函数。

头文件重复包含与名字重定义

#pragma once解决同一个编译单元重复包含,但无法解决不同编译单元重复定义的。在不同编译单元定义多个同名函数时,会链接出错;而定义多个同名类则不会报错:根据链接顺序选择其中一个。定义不同实现的同名类是ub。

可以用匿名命名空间或static解决重定义问题。使用非类型模板参数时,只能是整型,包括enum,或者是指向外部链接对象的指针。而匿名命名空间的成员是有外部链接的,而static声明没有外部链接。因此:

template <void fn()>
class Foobar {};

namespace {
   void func1() {};
}

static void func2() {};

int main() {
    Foobar<func1> a; // ok
    Foobar<func2> b; // error
}

链接属性,即linkage,表明一个符号的可见性范围。C++继承了C的链接属性,分为三种:

由于字符串文字是内部链接对象(因为两个具有相同名称但处于不同模块的字符串,是两个完全不同的对象),所以你不能使用它们来作为实参

template <char const* name>
class MyClass {
...
};
MyClass<"hell"> x;   //ERROR

另外,你也不能使用全局指针作为模版参数:

template <char const* name>
class MyClass {
...
};

char const* s = "hello";
MyClass<s> x;         //s是一个指向内部链接对象的指针

以上c++11后可以了

include+宏的小技巧

There is also the X Macro idiom which can be useful for DRY and simple code generation :

One defines in a header gen.x a kind of table using a not yet defined macro :

/** 1st arg is type , 2nd is field name , 3rd is initial value , 4th is help */
GENX( int , "y" , 1 , "number of ..." );
GENX( float , "z" , 6.3 , "this value sets ..." );
GENX( std::string , "name" , "myname" , "name of ..." );

Then he can use it in different places defining it for each #include with a usually different definition :

class X
{
public :

     void setDefaults()
     {
#define GENX( type , member , value , help )\
         member = value ;
#include "gen.x"
#undef GENX
     }

     void help( std::ostream & o )
     {
#define GENX( type , member , value , help )\
          o << #member << " : " << help << '\n' ;
#include "gen.x"
#undef GENX
     }

private :

#define GENX( type , member , value , help )\
     type member ;
#include "gen.x"
#undef GENX
}

单例模式的Double-Checked Locking Pattern

class Singleton {
public:
  static Singleton& GetInstance() {
    if (!instance_) {
      std::lock_guard<std::mutex> lock(mutex_);
      if (!instance_) {
        instance_.reset(new Singleton);
      }
    }
    return *instance_;
  }

  ~Singleton() = default;

private:
  Singleton() = default;

  Singleton(const Singleton&) = delete;
  Singleton& operator=(const Singleton&) = delete;

private:
  static std::unique_ptr<Singleton> instance_;
  static std::mutex mutex_;
};

https://blog.csdn.net/tantexian/article/details/50684689

事实上DCLP的方法也不能解决多线程环境的共享资源保护问题,其主要原因是 instance_.reset(new Singleton) 并非原子操作,编译器会将其转换成三条语句来实现:

  1. 为Singleton对象分配一片内存
  2. 构造一个Singleton对象,存入已分配的内存区
  3. 将pInstance指向这片内存区

编译器有时会交换步骤2和步骤3的执行顺序,导致线程不安全。

C/C++的编译器和链接器执行这些优化操作时,只会受到C/C++标准文档中定义的抽象机器上可见行为的原则这唯一的限制。

C++抽象机器上的可见行为包括:volatile数据的读写顺序,以及对输入输出(I/O)库函数的调用。将声明成volatile的数据作为左值来访问对象,修改对象,调用输入输出库函数,抑或调用其他有以上相似操作的函数,都会产生副作用(side effects),即执行环境状态发生的改变。

有一点很重要:这些抽象机器默认是单线程的。C/C++作为一种语言,二者都不存在线程这一概念,因此编译器在优化过程中无需考虑是否会破坏多线程程序。

既然如此,程序员怎样才能用C/C++写出能正常工作的多线程程序呢?通过使用操作系统特定的库来解决。例如Posix线程库(pthreads),这些线程库为各种同步原语的执行语义提供了严格的规范。由于编译器生成代码时需要依赖这些线程库,因此编译器不得不按线程库所约束的执行顺序生成代码。这也是为什么多线程库有一部分需要用直接用汇编语言实现,或者调用由汇编实现的系统调用(或者使用一些不可移植的语言)。换句话说,你必须跳出标准C/C++语言在你的多线程程序中实现这种执行顺序的约束。DCLP试图只使用一种语言来达到目的,所以DCLP不可靠。


C++11 保证静态局部变量的初始化过程是线程安全的。

在C++11中提供另一种方法,使得函数可以线程安全的只调用一次。即使用std::call_once和std::once_flag:

class Singleton {
public:
  static Singleton& GetInstance() {
    static std::once_flag s_flag;
    std::call_once(s_flag, [&]() {
      instance_.reset(new Singleton);
    });

    return *instance_;
  }

  ~Singleton() = default;

private:
  Singleton() = default;

  Singleton(const Singleton&) = delete;
  Singleton& operator=(const Singleton&) = delete;

private:
  static std::unique_ptr<Singleton> instance_;
};

条件变量 cv.wait

https://zhuanlan.zhihu.com/p/77999255

主线程修改ready后,通过cv.notify_one发出通知,然后通过cv.wait等待processed变量被置true。注意wait的写法。wait有两个重载函数,第一个只接受unique_lock,第二个还接受Predicate。

void wait( std::unique_lock<std::mutex>& lock );
template< class Predicate >
void wait( std::unique_lock<std::mutex>& lock, Predicate pred );

如果你以为Predicate版本的wait函数等价于如下的if判断,那你就错了。为了处理小概率的虚假唤醒,该版本的wait其实等价于while的实现版本:

if(!pred())
  wait(lock);

while(!pred())
  wait(lock);

为什么C++11引入了std::ref

函数模板 ref 与 cref 是生成 std::reference_wrapper 类型对象的帮助函数,它们用模板实参推导确定结果的模板实参。所以std::ref()返回的实际上是一个reference_wrapper而不是T&,可以从一个指向不能拷贝的类型的对象的引用生成一个可拷贝的对象。

std::reference_wrapper 的实例是对象(它们可被复制或存储于容器),但它们能隐式转换成 T& ,故能以之为以引用接收底层类型的函数的参数。

主要是考虑函数式编程(如std::bind)在使用时,bind()不知道生成的函数执行的时候,传递进来的参数是否还有效。所以它选择参数值传递而不是引用传递。

#include <functional>
#include <iostream>
 
void f(int& n1, int& n2, const int& n3)
{
    std::cout << "In function: " << n1 << ' ' << n2 << ' ' << n3 << '\n';
    ++n1; // increments the copy of n1 stored in the function object
    ++n2; // increments the main()'s n2
    // ++n3; // compile error
}
 
int main()
{
    int n1 = 1, n2 = 2, n3 = 3;
    std::function<void()> bound_f = std::bind(f, n1, std::ref(n2), std::cref(n3));
    n1 = 10;
    n2 = 11;
    n3 = 12;
    std::cout << "Before function: " << n1 << ' ' << n2 << ' ' << n3 << '\n';
    bound_f();
    std::cout << "After function: " << n1 << ' ' << n2 << ' ' << n3 << '\n';
}
Output:
Before function: 10 11 12
In function: 1 11 12
After function: 10 12 12

为什么每个编译器非得自己写一个标准库

c++标准库里的许多模板函数,是没法用c++代码写出来的。它自己的标准库(stl)不符合它自己的语法规范。你自己写代码山寨,是山寨不出这些函数的。只能把代码写在编译器里,让编译时用黑魔法来实现。所以做编译器时,就必须自己也做一个标准库,实现这些没法自举的模板函数。

当然,也有的编译器没自己写标准库,而是用操作系统原生编译器的库,但那样也很麻烦,得一个个系统手动适配对方的的黑魔法,识别出对方的这些没法自举的函数最后把调用转发到什么鬼地方,然后自己在编译时把它填好,而不是报未定义错误。

所以这些模板函数,在头文件里只看得到声明,看不到实现。在语言标准里,模板函数必须开放实现,不允许只有个声明。

以 VC 标准库 msvcrt 里的 std::is_union 为例:

// STRUCT TEMPLATE is_union
// determine whether _Ty is a union
template <class _Ty>
struct is_union : bool_constant<__is_union(_Ty)> {}; 

template <class _Ty>
_INLINE_VAR constexpr bool is_union_v = __is_union(_Ty);

这个模板函数是没法用 c++ 语法手动写出来的,也就是没法自举,所以标准库里就只是把它的调用转发到了一个根本连声明都追溯不到的 __is_union 里,相当于额外扩展了一个叫做 __is_union 的关键字,然后编译器把这个关键字用黑魔法实现,假装自己没有添加关键字,而是写的模板。

如果某个第三方编译器想用 msvcrt 而不是自己写,那么就得自己用黑魔法实现 __is_union ,避免编译时报错。而且还得紧跟 VC 更新,免得它某个版本不叫 __is_union 了于是又编译不对了。

boost loki 的文档:Without (some as yet unspecified) help from the compiler, we cannot distinguish between union and class types using only standard C++, as a result this type will never inherit from true_type

C++之父在设计和演化里说优先库函数,实在不行再加语言特性。很多分明应当作为语言builtin的东西,标准里面非得按模板去凑。最后的实现还是编译器自己扩展加了关键字,但非要装作是个函数而不是语言关键字。

libstdc++ 与 libc++

libstdc++是gcc搞的,libc++是llvm搞的,他们都是C++标准库的实现。

vector 与 array

vector和array的选择显然不是“在乎性能”。因为两个容器均能随机访问。如果需要的大小在运行时创建容器的时候就知道(不需要动态扩张),默认初始化vector然后reserve就行了,也不需要copy/move元素。

vector和array的下标都没有检查越界的,要检查越界用.at(),你要设计一个新容器,下标也不应该检查越界抛异常,而是应该实现.at()以符合标准库的风格,不给你的库用户制造surprise。

undefined behavior

程序行为不应该是由特定实现定义的,而是应该由接口描述定义的。

比如某个函数的描述是:输入0返回1。

这个描述其实可以理解成在任意状态(因为接口描述中没说在某个特定状态下,所以就可以理解为在任意状态下)下除了输入0的行为是定义的,在任意状态下其他输入的行为都是未定义的。

很显然未定义行为和什么程序语言,和是否崩溃,和是否抛异常是毫无关系的。更准确的说未定义行为和实现是毫无关系的,未定义行为指的是接口描述中所有未说明的情况的行为都是未定义行为。

把包含未定义行为的接口变成不包含未定义行的接口为的唯一方法是修改接口描述,这样这个函数在任意状态下的整个参数类型的自然定义域内就没有未定义行为了。

所以C/C++在研究它的特性的时候永远不能基于运行期的事实现象,而是应该基于文档和标准来编程。因为UB不是每次都会让你吃惊的,而且有时候因为程序小所以UB不会让你疼。

谁能彻底消灭UB,谁就能解决停机问题。


Algorithms that take a single iterator denoting a second sequence assume that the second sequence is at least as large at the first

以下代码是不正确/不规范的,因为存在越界问题:

vector<int> vec1 = {1, 2};
vector<int> vec2 = {1};
equal(vec1.cbegin(), vec1.cend(), vec2.cbegin());

标准里在 equal 条目里有写 last2 不提供的版本使用 first2 + (last1 - first1)

在链接过程中,如果某个目标文件中的符号被用到了,那么这个目标文件会单独从库文件中提取出来并入可执行程序,而其余的目标文件则会被丢弃。因此在链接静态库的时候,应该将被依赖的库放在最右边。

在windows上,默认情况下编译的dll是不导出符号的,所以要手动添加dllexport。

但在*nix下,默认编译的so是会导出所有符号的,所以大家就不用考虑区分静态库/动态库,import/export,很是方便。不过这样带来的弊病就是,遇上重复的符号时,你也不知道编译器会选择哪一个,于是就存在冲突或者bug的可能。

-fvisibility=hidden__attribute__((visibility("default")))的使用

class template argument deduction

看上去只是 function template argument deduction 的延伸,但实际上它夹带的 deduction guide 特性,才是 main feature。

如果给这个特性换个名字,叫 specialization selector, 你就明白了。比如你有这么一个模板类,

template <typename Queue, typename F>
class Tasks
{
    Tasks(Queue&& q, F&& on_finished);
};

本意是允许用户自己定制 Callback 的类型,结果用户写Tasks(v, []{ cout << ...; }); 得到一堆各不相同的 Tasks<T, lambda>, 放不进同质容器。

但只要有这样一个 guidetemplate

<typename Queue, typename Fn>
Tasks(Queue, Fn) -> Tasks<Queue, std::function<void()>>;

用户写上面的代码时就会选择到 Tasks<Queue, std::function<void()>> 这个 specialization, 然后尝试用 lambda 初始化std::function<void()>, 达成「用户可以自己指定 Callback 类型,默认使用 type erasure 」,给 API 增加很多弹性。

tie 和 structured bingdings

Structured bindings has specific language rules to handle arrays and certain other types. tie() is specifically a tuple<T&…> and can only be assigned from another tuple<U&…>.

vector

vector<bool> 有两个问题.

一个东西要成为STL容器,必须满足所有列于C++标准23.1节的容器要求。在这些要求中,有这样一条:如果C是一个T类型元素容器,并且C支持operator[]。那么以下代码必须能够编译:

T *p = &c[0];   // initialize a T* with the address
                // of whatever operator[] returns

标准库提供了两个替代品,它们满足几乎所有的需求:

迭代器失效

vector迭代器失效问题总结:

vector的erase操作可以返回下一个有效的迭代器,所以每次执行删除操作的时候,将下一个有效迭代器返回

deque迭代器失效总结:

对于关联容器(如map, set,multimap,multiset),删除当前的iterator,仅仅会使当前的iterator失效,只要在erase时,递增当前iterator即可。

static_assert(false)

https://zhuanlan.zhihu.com/p/371769440

直接static_assert(false)是不可以的,因为编译器解析这个语句时就会报错,即使分支不会走到。修改如下:

template <typename> constexpr bool false_c = false; 

template<typename T>
void f()
{
    if constexpr (false)
        static_assert(false_c<T>);

    if constexpr (false)
        []<bool flag = false>()
            {static_assert(flag, "no match");}();
}

v8中的用法:

template <typename T>
struct TypeInfoHelper {
    static_assert(sizeof(T) != sizeof(T), "This type is not supported");
};

#define SPECIALIZE_GET_TYPE_INFO_HELPER_FOR(T, Enum)                              \
    template <>                                                                   \
    struct TypeInfoHelper<T> {                                                    \
        static constexpr CTypeInfo::Flags Flags() {                               \
            return CTypeInfo::Flags::kNone;                                       \
        }                                                                         \
                                                                                  \
        static constexpr CTypeInfo::Type Type() { return CTypeInfo::Type::Enum; } \
    };

#define BASIC_C_TYPES(V)        \
    V(void, kVoid)              \
    V(bool, kBool)              \
    V(int32_t, kInt32)          \
    V(uint32_t, kUint32)        \
    V(int64_t, kInt64)          \
    V(uint64_t, kUint64)        \
    V(float, kFloat32)          \
    V(double, kFloat64)         \
    V(ApiObject, kApiObject)    \
    V(v8::Local<v8::Value>, kV8Value)

BASIC_C_TYPES(SPECIALIZE_GET_TYPE_INFO_HELPER_FOR)

侵入式容器

已知struct type和type的成员变量field,可以得到field在A中的偏移:

((ptrdiff_t)(&((type *)0)->field))

若有一个指向某type实例中field成员的指针,可以得到该type势力:

((type *)((void *)(ptr) - offsetof(type, field))))

侵入式容器中,链表节点不需要存储数据结构体,而是让数据结构体存储链表节点。一个对象能同时存在于多个链表中,只需要存储多个节点。节点的生命周期独立于容器而存在。

linux内核的链表是侵入式(intrusive),就是和一般的数据结构书上教的差不多,每个节点本身包含了next/prev指针。而STL的链表属于非侵入式(non-intrusive),你可以放里面放任何类型,不需要含有next/prev指针。

标准库中list帮你维护对象的生命周期,自己不需要维护前向和后向指针,标准库全部帮你搞定。侵入式的链表,对象的生命周期需要自己管理——如果对象被释放,但链表里还挂着这个对象,那随后的崩溃几乎就不可避免的了。

侵入式链表的主要好处就在于你需要把链表结点挂到多个容器里的时候,删除结点时你需要同时从多个容器里删除。一种可能的场景是你使用一个 std::map 来缓存某个计算的结果,同时用一个按最近使用时间排序(LRU)的链表来决定从哪儿开始丢弃最老的结果。这种情况下,使用 std::list 一般意味着你需要放对象的指针或智能指针到链表里,这样,在链表遍历时就会多一次间接,同时链表里的每一项也意味着额外的内存分配。你很可能也做不到彻底的不侵入,而必须在对象里放上一个 list::iterator 来允许今后从容器里释放自己。

uc/os里面调度代码中,调度队列里面保存的是每个pcb中一个成员变量的地址,通过这种方式根据成员变量地址获取结构体首地址。

函数的实现放在头文件中,避免重复定义的链接错误方法

注意msvc允许inline函数仅有前置声明,而Clang遵循标准规定会报出链接错误。

static在c++中的用法

静态成员变量、静态成员函数、静态局部变量、内部链接的函数

string_view

使用string_view要确保引用的字符串生命周期比string_view长

noexcept

在C++ 11中类结构隐式自动声明的或者是由程序员主动声明的不带有任何修饰符的析构函数,都会被编译器默认带上noexcept (true)标记,以表示这个析构函数不会抛出异常。

如果一个类的父类析构函数或者它的成员函数被标记为了可抛出异常,那么这个类的析构函数就会默认被标记为可抛出异常,也就我们所说的受到了污染。

STL为了保证容器类型的内存安全,在大多数情况下只会调用被标记为不会抛出异常的移动构造函数,否则会调用其拷贝构造函数来作为替代。所以move constructor/assignment operator 如果不会抛出异常,一定用noexcept。

当vector中的元素的移动拷贝构造函数是noexcept时,vector就不会使用copy方式,而是使用move方式将旧容器的元素放到新容器中:

两阶段翻译 Two-Phase Translation

模板会分成两个阶段进行”编译“:

  1. 在不进行模板instantiation的definition time阶段,此时会忽略模板参数,检查如下方面:
    • 语法错误,包括缺失分号。
    • 使用未定义参数。
    • 如果static assertion不依赖模板参数,会检查是否通过static assertion.
  2. 在instantiation阶段,会再次检查模板里所有代码的正确性,尤其是那些依赖模板参数的部分。

多个模板函数返回值类型

template<typename T1, typename T2>
T1 max (T1 a, T2 b) {
    return b < a ? a : b;
}
// 注意:返回类型总是T1
auto m = max(4, 7.2);       

解决方法:

  1. 引入额外模板参数作为返回值类型
    template<typename T1, typename T2, typename RT>
    RT max(T1 a, T2 b);
    

    当模板参数不能根据传递的参数推导出来时,我们就需要显式的指定模板参数类型。RT是不能根据函数的参数列表推导出来的,所以我们需要显式的指定

  2. 让编译器自己找出返回值类型
    template <typename T1, typename T2>
    auto max(T1 a, T2 b) -> decltype(b < a ? a : b) {
      return b < a ? a : b;
    }
    // 这里的重点不是计算返回值,而是得到返回值类型
    template <typename T1, typename T2>
    auto max(T1 a, T2 b) -> decltype(true ? a : b) {      
      return b < a ? a : b;
    }
    // C++14中可以省略trailing return type
    template<typename T1, typename T2>
    auto max (T1 a, T2 b) {
     return b < a ? a : b;
    }
    
  3. 将返回值声明为两个模板参数的公共类型
    template <typename T1, typename T2>
    typename std::common_type<T1, T2>::type max(T1 a, T2 b) {
      return b < a ? a : b;
    }
    // C++14
    template <typename T1, typename T2>
    std::common_type_t<T1, T2> max(T1 a, T2 b) {     
      return b < a ? a : b;
    }
    template <typename T1, typename T2, typename RT = std::common_type_t<T1, T2>>
    RT max(T1 a, T2 b) {
      return b < a ? a : b;
    }
    

模板参数类型推导过程中不允许类型自动转换

int max(int a, int b) { 
  return b < a ? a : b; 
}
template <typename T> 
T max(T a, T b) { 
  return b < a ? a : b; 
}
// calls the nontemplate for two ints
max('a', 42.7);     

函数名加括号

避免匹配到宏。WinDef.h 中定义了两个宏 max 和 min,如需要自己定义max、min,需要加括号

#define sin(x) __builtin_sin(x)

// parentheses avoid substitution by the macro
double (sin)(double arg) {
    return sin(arg); // uses the macro
}

int main() {
    // uses the macro
    printf("%f\n", sin(3.14));

    // uses the function
    double (*x)(double) = &sin;

    // uses the function
    printf("%f\n", (sin)(3.14));
}

折叠表达式 Fold Expressions

template <typename FirstType, typename... Args>
void print(FirstType first, Args... args) {
  std::cout << first;

  auto printWhiteSpace = [](const auto arg "") { std::cout << " " << arg; };

  (..., printWhiteSpace(args));
}

(..., printWhiteSpace(args));会被展开为:printWhiteSpace(arg1), printWhiteSpace(arg2), printWhiteSpace(arg3)

可变模板参数展开

注意与折叠表达式的区别

template<typename... T>
void printDoubled (T const&... args) {
  print (args + args...);
}

printDoubled(7.5, std::string("hello"), std::complex<float>(4,2));

上面的调用会展开为:

print(7.5 + 7.5,
std::string("hello") + std::string("hello"),
std::complex<float>(4,2) + std::complex<float>(4,2);

Variadic Base Classes and using

class Customer {
private:
  std::string name;

public:
  Customer(std::string const &n) : name(n) {}
  std::string getName() const { return name; }
};

struct CustomerEq {
  bool operator()(Customer const &c1, Customer const &c2) const {
    return c1.getName() == c2.getName();
  }
};

struct CustomerHash {
  std::size_t operator()(Customer const &c) const {
    return std::hash<std::string>()(c.getName());
  }
};

// define class that combines operator() for variadic base classes:
template <typename... Bases> struct Overloader : Bases... {
  using Bases::operator()...; // OK since C++17
};

int main() {
  // combine hasher and equality for customers in one type:
  using CustomerOP = Overloader<CustomerHash, CustomerEq>;
  std::unordered_set<Customer, CustomerHash, CustomerEq> coll1;
  std::unordered_set<Customer, CustomerOP, CustomerOP> coll2;
  ...
}

overloaded的实现

template<class... Ts> struct overloaded : Ts... { using Ts::operator()...; };
template<class... Ts> overloaded(Ts...) -> overloaded<Ts...>;

overloaded s{
    [](int){cout << "int" << endl;},
    [](double){cout << "double" << endl;},
    [](string){cout << "string" << endl;},
};
s(1); // int
s(1.); // double
s("1"); // string

参数推导规则

https://zhuanlan.zhihu.com/p/338788455 https://zhuanlan.zhihu.com/p/338798151

名称查找与ADL

https://zhuanlan.zhihu.com/p/338917913

对于 qualified name 来说,会有显示指明的作用域。如果作用域是一个类,那么基类也会被考虑在内,但是类的外围作用域不会被考虑。相反,对于非 qualified name 来说,会在外围作用域逐层查找(假如在类成员函数中,会先找本类和基类的作用域)。这叫做 ordinary lookup

template<typename T>
T max (T a, T b) {
    return b < a ? a : b;
}

namespace BigMath {
  class BigNumber {
    ...
};

  bool operator < (BigNumber const&, BigNumber const&);
  ...
}

using BigMath::BigNumber;

void g (BigNumber const& a, BigNumber const& b) {
  ...
  BigNumber x = ::max(a,b);
  ...
}

这里的问题是:当调用 max 时,ordinary lookup不会找到 BigNumber 的operator <。如果没有一些特殊规则,那么在 C++ namespace 场景中,会极大的限制模板的适应性。ADL 就是这个特殊规则,用来解决此类的问题。

需要注意的一点是,ADL 会忽略 using:

namespace X {
  template <typename T> void f(T);
}

namespace N {
  using namespace X;
  enum E { e1 };
  void f(E) { std::cout << "N::f(N::E) called\n"; }
}    // namespace N

void f(int) { std::cout << "::f(int) called\n"; }

int main() {
  ::f(N::e1);    // qualified function name: no ADL
  f(N::e1);     // ordinary lookup finds ::f() and ADL finds N::f(), the latter is preferred
}
int main() {
    std::cout << "Test\n"; // There is no operator<< in global namespace, but ADL
                           // examines std namespace because the left argument is in
                           // std and finds std::operator<<(std::ostream&, const char*)
    operator<<(std::cout, "Test\n"); // same, using function call notation

    // however,
    std::cout << endl; // Error: 'endl' is not declared in this namespace.
                       // This is not a function call to endl(), so ADL does not apply

    endl(std::cout); // OK: this is a function call: ADL examines std namespace
                     // because the argument of endl is in std, and finds std::endl

    (endl)(std::cout); // Error: 'endl' is not declared in this namespace.
                       // The sub-expression (endl) is not a function call expression
}

注意最后一点(endl)(std::cout);,如果函数的名字被括号包起来了,那也不会应用 ADL。

namespace A {
      struct X;
      struct Y;
      void f(int);
      void g(X);
}

namespace B {
    void f(int i) {
        f(i);      // calls B::f (endless recursion)
    }
    void g(A::X x) {
        g(x);   // Error: ambiguous between B::g (ordinary lookup)
                //        and A::g (argument-dependent lookup)
    }
    void h(A::Y y) {
        h(y);   // calls B::h (endless recursion): ADL examines the A namespace
                // but finds no A::h, so only B::h from ordinary lookup is used
    }
}
namespace c1
{
    namespace c2
    {
        struct cc{};
        void f(cc& o){}             //#1
    }
}
void f(c1::c2::cc& o){}
namespace f1
{
    namespace f2
    {
        void f(const c1::c2::cc& o){}   //#2
        void g()
        {
            c1::c2::cc o;
            const c1::c2::cc c(o);
            f(o);
            f(c);
        }
        void f(c1::c2::cc& o){}     //#3
    }
}

因为#3是定义于g的后面,所以在g中是不可见的。全局函数::f被#2隐藏(name hiding)。因此对于f(o)来说,我们通过使用ADL可以调用#1,我们通过name hiding也可以调用#2,但是我们最后调用最佳匹配#1。而对于f(c)我们通过同样的分析,我们知道调用#2。

依赖型模板名称

通常而言, 编译器会把模板名称后面的<当做模板参数列表的开始,否则,<就是比较运算符。当引用的模板名称是 Dependent Name 时,编译器不会假定它是一个模板名称,除非显示的使用 template 关键字来指明,模板代码中常见的->template、.template、::template就应用于这种场景中。

template<unsigned long N>
void printBitset (std::bitset<N> const& bs) {
    std::cout << bs.template to_string<char, std::char_traits<char>, std::allocator<char>>();
}

这里,参数 bs 依赖于模板参数 N。所以,我们必须通过 template 关键字让编译器知道 bs 是一个模板名称

模板的模板参数匹配deque、vector

// error
template <typename T, template <typename> class Cont = std::deque>
class Stack {
  ...
};

std::deque和Cont不匹配。标准库的std::deque有两个参数,还有一个默认参数 Allocator。

  1. 将 Cont 和 std::deque 的参数匹配即可
  2. 可变参数模板
    template <typename T,
           template <typename......>
           class Cont = std::deque>
    class Stack {
    ......
    };
    

    但是,这点对于std::array无效,因为 std::array 的第二个参数是非类型模板参数

inline

https://blog.csdn.net/Hello_World_156_5634/article/details/90300356


https://www.zhihu.com/question/40793741 static function。你可以把一个函数标记为static(也称为internal linkage),这样该函数的symbol就会被隐藏,从而该函数只存在在当前translation unit。换了下一个translation unit之后,该函数被忘得一干二净。linker也从来不知道这函数存在过。这时候你就算再定义一次上次translation已经定义过的static函数,linker也不会报redefinition错误。当然,这样代码在binary中就出现了多次。

内联优化有个缺陷,就是在同一个translation unit里一定要看到函数体,所以光看到declaration是没用的。现在考虑这么个问题:传统的在头文件中声明,在一个文件(.c)中实现函数体的方式有时执行太慢了。为什么慢呢,假设我这个函数就一行,但是函数调用的压栈传参数弹栈跳转等指令占了大部分开销,真是太不合算了。 这时候在传统C里面有两个解决方案: 1) “宏函数”。就是把本来藏在.c文件里的函数体放到一个宏里面去,当然宏也在头文件里。然后大家include头文件的时候就把宏也include走了,使用宏的时候就把这段代码一遍遍展开到所有使用的地方,消除了函数调用的开销。 2) 在编译器支持内联优化的情况下,在头文件里定义static function。任何别的.c文件,只要include了你的头文件,都对你的头文件做了一次复制粘贴,自动获得了该static function的函数体。所以在不同的translation unit里面,这些函数并不冲突,因为它们是static的。值得一提的是,这些函数体不一定一模一样。

inline关键字不仅编译器认识,而且编译器在没有真正内联该函数时,会通过某种方式提示linker说这个函数被标记为“可重复定义”耶 - 根据我用gcc的实验,生成的是一个weak symbol。当linker看到一个weak symbol,会把函数名写在一个小本本上。在linker最后把所有文件link到一起的时候,它会把小本本扫一遍,对于同名函数只留一个,别的函数连带函数体统统删掉。这样就解决了binary size bloat的问题。当然这只是一种典型实现方式,并非一定如此。 另外,在编译器真正内联了该函数的时候,效果就和static一样了,这也是为什么你的代码里找不到定义 - 因为linker根本看不到static函数。

调用约定

__cdecl是C和C++程序的默认调用约定:参数通过堆栈来传递,从右向左依次入栈,由调用者平衡堆栈。

__stdcall的调用约定是参数通过堆栈来传递,从右向左依次入栈,由被调用者平衡堆栈。一般Windows API函数都是__stdcall。

__fastcall的调用约定是:第一个参数通过ECX传递,第二个参数通过EDX传递,第三个参数起从右向左依次入栈,由被调用者平衡堆栈。

注意入栈顺序不等于求值顺序。

std::conditional

不同于常规的if-then-else语句,这里所有分支的模板实参在被选择前都会被计算,所以不能有非法的代码

// T是bool或非整型将产生未定义行为
template<typename T>
struct UnsignedT {
    using Type = std::conditional_t<std::is_integral_v<T> && !std::is_same_v<T, bool>,
        std::make_unsigned_t<T>, T>; // 无论是否被选择,所有分支都会被计算
};

添加一个类型函数作为中间层即可解决此问题

// yield T when using member Type:
template<typename T>
struct IdentityT {
    using Type = T;
};

// to make unsigned after IfThenElse was evaluated:
template<typename T>
struct MakeUnsignedT {
    using Type = std::make_unsigned_t<T>;
};

template<typename T>
struct UnsignedT {
    using Type = std::conditional_t<std::is_integral_v<T> && !std::is_same_v<T, bool>,
        MakeUnsignedT<T>, IdentityT<T>>;
};

别名模板template<typename T> using MakeUnsigned = typename MakeUnsignedT<T>::Type;并不能有效地用于conditional的分支。 使用别名模板总会实例化类型,将使得对给定类型难以避免无意义的实例化traits。

用于类模板的标签分派

https://downdemo.gitbook.io/cpp-templates-2ed/part3-mo-ban-yu-she-ji/17.-ji-yu-lei-xing-shu-xing-de-zhong-zai-overloading-on-type-property/lei-te-hua/yong-yu-lei-mo-ban-de-biao-qian-fen-pai

// construct a set of f() overloads for the types in Types...:
template<typename... Types>
struct A;

// basis case: nothing matched:
template<>
struct A<> {
    static void f(...); // 如果下面对所有参数尝试匹配失败,则匹配此版本
};

// recursive case: introduce a new f() overload:
template<typename T1, typename... Rest>
struct A<T1, Rest...> : public A<Rest...> { // 递归继承
    static T1 f(T1); // 尝试匹配第一个参数,如果匹配则返回类型T1就是所需Type
    using A<Rest...>::f; // 否则递归调用自身基类,尝试匹配下一个参数
};

// find the best f for T in Types...:
template<typename T, typename... Types>
struct BestMatchInSetT { // f的返回类型就是所需要的Type
    using Type = decltype(A<Types...>::f(std::declval<T>()));
};

template<typename T, typename... Types>
using BestMatchInSet = typename BestMatchInSetT<T, Types...>::Type;

空基类优化

确保两个不一样的对象拥有不同的地址,C++ 中空类对象占一个字节。

空类最常用于作为基类,那时候为了对齐实际可能占4个字节或以上。但这样会浪费空间,尤其是多重继承多个空基类的時候。所以编译器有空基类优化(empty base class optimization, EBCO),令无非静态数据成员、无虚函数的基类实际占0字节。

空基类只在没有歧义的情况下会优化,如果 class A{}; class B:A { A a; };那就没有空基类优化,因为如果优化的话,B类的基类A和B类的的成员a就有了相同的地址,这是不合理的。

结合Barton-Nackman Trick与CRTP的运算符实现

https://downdemo.gitbook.io/cpp-templates-2ed/part3-mo-ban-yu-she-ji/18.-mo-ban-yu-ji-cheng-template-and-inheritance/qi-yi-di-gui-mo-ban-mo-shi-the-curiously-recurring-template-patterncrtp/jie-he-bartonnackman-trick-yu-crtp-de-yun-suan-fu-shi-xian

template<typename T>
class A {
public:
    friend bool operator!=(const T& x1, const T& x2) {
        return !(x1 == x2);
    }
};

class X : public A<X> {
public:
    friend bool operator==(const X& x1, const X& x2) {
        // implement logic for comparing two objects of type X
    }
};

int main() {
    X x1, x2;
    if (x1 != x2) {}
}

内联函数和constexpr函数可以在程序中定义不止一次

对于某个给定的内联函数或者constexpr函数来说,它的多个定义必须完全一致。基于这个原因,内联函数和constexpr函数通常定义在头文件中

能定义不止一次的好处是方便你放到头文件里,放到头文件里的好处是每个include这个头文件的.c文件都能看到函数体,看到函数体的好处是编译器可以内联。内联的好处是代码变快了。另外,所有函数体定义必须一模一样,不然出了问题概不负责。constexpr自带inline属性

内存模型

https://blog.csdn.net/pongba/article/details/1659952

现有的单线程内存模型没有对编译器做足够的限制,从而许多我们看上去应该是安全的多线程程序,由于编译器不知道(并且根据现行标准(C++03)的单线程模型,编译器也无需关心)多线程的存在,从而可能做出不违反标准,但能够破坏程序正确性的优化(这类优化一旦导致错误便极难调试,基本属于非查看生成的汇编代码不可的那种)。

Observable Behavior

标准把Observable Behavior(可观察行为)定义为volatile变量的读写和I/O操作。原因也很简单,因为它们是Observable的。volatile变量可能对应于memory mapped I/O,所有I/O操作在外界都有可观察的效应,而所有内存内的操作都是不显山露水的,可以尽情优化

name mangling

由于 c 语言和 c++ 的 name mangling 方式是不同的,所以就会产生一个问题:如果在 c++ 语言中调用 c 的函数,就会因为在链接时找不到函数而产生错误。解决的办法就是在用到的 c++ 函数之前加上关键字 extern “C”,这样 c++ 的函数就会按照 c 语言的 name mangling 方式,链接便能正确执行。

c++filt指令 ,看name mangling后的函数名

Unevaluated Operands

https://blog.csdn.net/IndignantAngel/article/details/44015361

C++98标准中的Unevaluated Operands,只有sizeof操作符。C++11又引入了decltype,typeid和noexcept。Unevaluated Operands不会有求值的计算,即使是在编译期间。这意味着Unevaluated Operands 中的表达式甚至不会生成具体的C++代码,操作符中的表达式仅需要声明而无需定义。在模板元编程中,我们有时候经常仅需要对catch a glimpse of一个表达式,而这些操作符都是很好的工具。

namespace cpp11 {	
    template <typename T>	
    struct is_copy_assignable {	
    private:		
        template <typename U, typename = decltype(std::declval<T&>() = std::declval<T const&>())>	
        static std::true_type try_assign(U&&); 		

        static std::false_type try_assign(...); 	
    public:		
        using type = decltype(try_assign(std::declval<T>()));	
    };
}

namespace cpp98
{
	namespace detail
	{
		template <typename T>
		T declval();
	}

	template <typename T>
	struct is_copy_assignable
	{
	private:
		typedef char One;
		typedef struct { char a[2]; } Two;

		template <int N = sizeof(detail::declval<T&>() = detail::declval<T const&>())>
		static One try_assign(int);

		static Two try_assign(...);

	public:
	typedef typename std::conditional<sizeof(try_assign(0)) == sizeof(One),
			std::true_type, std::false_type>::type type;
	};
}
namespace cpp1y
{
	template <typename ... Args>
	struct make_void 
	{
		typedef void type;
	};
	
	template <typename ... Args>
	using void_t = typename make_void<Args...>::type;
 
	template <typename T, typename = void>
	struct is_copy_assignable : std::false_type
	{};
 
	template <typename T>
	struct is_copy_assignable<T, void_t<decltype(std::declval<T&>() = std::declval<T const&>())>> : std::true_type
	{};
}

运用ADL扩展模板库

https://blog.csdn.net/IndignantAngel/article/details/70207260

C++的语言规范不允许在一个命名空间中特化另一个命名空间的类模板,也不允许在一个命名空间下特化全局命名空间的类模板。

// 这里是你的库类中需要扩展的组件
namespace lib
{
    void to_extend(...) {}
}

// 客户处的扩展代码
namespace client
{
    struct foo {};

    // 下面的代码通常会被宏来生成
    auto to_extend(foo const&)
    {
        struct foo_extend
        {
            static constexpr char const* name() { return "foo"; }
        };

        return foo_extend{};
    }
}

// 在库中,可能是这样集成组件的
namespace lib
{
    constexpr char const* apply_impl(std::true_type, ...)
    {
        return "null";
    }

    template <typename T>
    constexpr char const* apply_impl(std::false_type, T)
    {
        return T::name();
    }

    template <typename T>
    constexpr char const* apply(T const& t)
    {
        using type = decltype(to_extend(t));        // 探测有没有对应的to_extend函数,ADL查找保证
        using is_void = std::is_same<type, void>;
        return apply_impl(is_void{}, type{});
    }
}

// 在client中可能是这样使用的
namespace client
{
    void some_function()
    {
        foo f;
        std::cout << lib::apply(f) << std::endl;
    }
}

简单解释一下,下面的代码是如何工作的。首先,函数的 ADL 查找,是 apply 函数尝试调用 to_extend 的时候,不仅会查找 lib 命名空间下的符号,也会去查找 T 类型所在命名空间的符号。我们定义的 to_extend 函数在 client 命名空间下,foo 类型也在 client 命名空间下。那么 ADL 查找肯定可以找到 to_extend 函数的符号。然后,我们没有选择类模板的特化,而是选择了使用 to_extend 函数,返回一个它内部定义的类型作为 policy 的功能。

模板静态成员的定义及实例化

https://www.cnblogs.com/tangzhenqiang/p/4332801.html

template <typename T> class Test{
public:
    static std::string info;
};

template <> string Test<int>::info("123");          //ok
template <typename T> string Test<T>::info("123");  //ok
template <typename T> string Test<T>::info;         //ok
template <> string Test<int>::info;                 //error,编译器认为它是一个声明而非定义。
template <> string Test<int>::info();               //error,声明一个函数
template <typename T> string Test<T>::info();       //error,声明一个函数

一般为了避免无法编译,应当尽量减少使用如下方式的定义 template <typename T> string Test<T>::info; 只有在你首次需要使用时在实现文件中给出如下特化定义即可,其他文件只要包含头文件就能使用。 template <> string Test<int>::info("123");


Older · View Archive (37)

Webbench

Newer

又是第一篇日志