你好,我是吴咏炜。这一讲我为你整理了本专栏部分课后思考题的答案,给你作为参考。

第 2 讲

你觉得智能指针应该满足什么样的线程安全性?

答:(不是真正的回答,只是描述一下标准中的智能指针的线程安全性。)

  1. 多个不同线程同时访问不同的智能指针(不管是否指向同一个对象)是安全的。
  2. 多个不同线程同时读取同一个智能指针是安全的。
  3. 多个不同线程在同一个智能指针上执行原子操作(atomic_load 等)是安全的。
  4. 多个不同线程根据同一个智能指针创建新的智能指针(增加引用计数)是安全的。
  5. 只会有一个线程最后会(在引用计数表示已经无引用时)调用删除函数去销毁存储的对象。

其他操作潜在是不安全的,特别是在不同的线程对同一个智能指针执行 reset 等修改操作。

第 3 讲

为什么 smart_ptr::operator= 对左值和右值都有效,而且不需要对等号两边是否引用同一对象进行判断?

答:我们使用值类型而非引用类型作为形参,这样实参永远会被移动(右值的情况)或复制(左值的情况),不可能和 *this 引用同一个对象。

第 4 讲

为什么 stack(或 queue)的 pop 函数返回类型为 void,而不是直接返回容器的 top(或 front)成员?

答:这是 C++98 里、还没有移动语义时的设计。如果 pop 返回元素,而元素拷贝时发生异常的话,那这个元素就丢失了。因而容器设计成有分离的 top(或 front)和 pop 成员函数,分别执行访问和弹出的操作。

有一种可能的设计是把接口改成 void pop(T&),这增加了 T 必须支持默认构造和赋值的要求,在单线程为主的年代没有明显的好处,反而带来了对 T 的额外要求。

第 5 讲

为什么大部分容器都提供了 beginend 等方法?

答:容器提供了 beginend 方法,就意味着是可以迭代(遍历)的。大部分容器都可以从头到尾遍历,因而也就需要提供这两个方法。

为什么容器没有继承一个公用的基类?

答:C++ 不是面向对象的语言,尤其在标准容器的设计上主要使用值语义,使用公共基类完全没有用处。

第 7 讲

目前这个输入行迭代器的行为,在什么情况下可能导致意料之外的后果?

答:

#include <fstream>
#include <iostream>
#include "istream_line_reader.h"

using namespace std;

int main()
{
  ifstream ifs{"test.cpp"};
  istream_line_reader reader{ifs};
  auto begin = reader.begin();
  for (auto it = reader.begin();
    it != reader.end(); ++it) {
    cout << *it << '\n';
  }
}

以上代码,因为 begin 多调用了一次,输出就少了一行……

请尝试一下改进这个输入行迭代器,看看能不能消除这种意外。如果可以,该怎么做?如果不可以,为什么?

答:很困难。比如,文件如果为空的话,从迭代器的行为角度,begin() 应该等于 end()——不预先读取一次的话,就无法获知这个结果。这样的改造总体看起来很不值,因此一般都不会选择这样做。

第 10 讲

这讲里我们没有深入讨论赋值;请你思考一下,如果例子里改成赋值,会有什么样的变化?

答:返回对象部分的讨论没有变化。对象的移动赋值操作应当实现成无异常,以确保数据不会丢失。

返回值优化在赋值情况下会失效。更一般的情况下,除非需要持续更新某个变量,比如在 vector 尾部追加数据,尽量对变量进行一次性赋值、不后续修改。这样的代码更容易推理,更不容易在后续修改中出错,也更能让编译器做(返回值)优化。

第 11 讲

为什么说 UTF-32 处理会比较简单?

答:UTF-32 下,一个字符就是一个基本的处理单位,一般不会出现一个字符跨多个处理单位的情况(UTF-8 和 UTF-16 下会发生)。

你知道什么情况下 UTF-32 也并不那么简单吗?

答:Unicode 下有所谓的修饰字符,用来修饰前一个字符。按 Unicode 的处理规则,这些字符应该和基本字符一起处理(如断行之类)。所以 UTF-32 下也不可以在任意单位处粗暴断开处理。

哪种 UTF 编码方式空间存储效率比较高?

答:视存储的内容而定。

比如,如果内容以 ASCII 为主(如源代码),那 UTF-8 效率最高。如果内容以一般的中文文本为主,那 UTF-16 效率最高。

第 12 讲

为什么并非所有的语言都支持这些不同的多态方式?

答:排除设计缺陷的情况,语言支持哪些多态方式,基本上取决于语言本身在类型方面的特性。

以 Python 为例,它是动态类型的语言。所以它不会有真正的静态多态。但和静态类型的面向对象语言(如 Java)不同,它的运行期多态不需要继承。没有参数化多态初看是个缺陷,但由于 Python 的动态参数系统允许默认参数和可变参数,并没有什么参数化多态能做得到而 Python 做不到的事。

第 17 讲

想一想,你如何可以实现一个惰性的过滤器?

答:

#include <iterator>

using namespace std;

template <typename I, typename F>
class filter_view {
public:
  class iterator {
  public:
    typedef ptrdiff_t
      difference_type;
    typedef
      typename iterator_traits<
        I>::value_type value_type;
    typedef
      typename iterator_traits<
        I>::pointer pointer;
    typedef
      typename iterator_traits<
        I>::reference reference;
    typedef forward_iterator_tag
      iterator_category;

    iterator(I current, I end, F cond)
      : current_(current)
      , end_(end)
      , cond_(cond)
    {
      if (current_ != end_ &&
          !cond_(*current_)) {
        ++*this;
      }
    }
    iterator& operator++()
    {
      while (current_ != end_) {
        ++current_;
        if (cond_(*current_)) {
          break;
        }
      }
      return *this;
    }
    iterator operator++(int)
    {
      auto temp = *this;
      ++*this;
      return temp;
    }
    reference operator*() const
    {
      return *current_;
    }
    pointer operator->() const
    {
      return &*current_;
    }

    bool operator==(const iterator& rhs)
    {
      return current_ == rhs.current_;
    }
    bool operator!=(const iterator& rhs)
    {
      return !operator==(rhs);
    }

  private:
    I current_;
    I end_;
    F cond_;
  };
  filter_view(I begin, I end,
              F cond)
    : begin_(begin)
    , end_(end)
    , cond_(cond)
  {}
  iterator begin() const
  {
    return iterator(begin_, end_, cond_);
  }
  iterator end() const
  {
    return iterator(end_, end_, cond_);
  }

private:
  I begin_;
  I end_;
  F cond_;
};

第 18 讲

我展示了 compose 带一个或更多参数的情况。你觉得 compose 不带任何参数该如何定义?它有意义吗?

答:

inline auto compose()
{
    return [](auto&& x) -> decltype(auto)
    {
        return std::forward<decltype(x)>(x);
    };
}

这个函数把参数原封不动地传回。它的意义相当于加法里的 0,乘法里的 1。

在普通的加法里,你可能不太需要 0;但在一个做加法的地方,如果别人想告诉你不要做任何操作,传给你一个 0 是最简单的做法。

有没有可能不用 index_sequence 来初始化 bit_count?如果行,应该如何实现?

答:可以,但这里有个小陷阱,目前 constexpr 要求在构造时直接初始化对象的内容。

template <size_t N>
struct bit_count_t {
  constexpr bit_count_t()
  {
    for (auto i = 0U; i < N; ++i) {
      count[i] = count_bits(i);
    }
  }
  unsigned char count[N]{};
};

constexpr bit_count_t<256>
  bit_count;

也可以考虑改用 std::array

std::array<char, N> count{};

使用 std::array 的话,我们可以考虑不再需要 bit_count_t 结构,因为可以让 get_bit_count 直接返回一个 std::array<char, 256>(返回 char [256] 则不可行)。

到了 C++20,允许 constexpr 对象里存在平凡默认构造的成员之后,就可以去掉数组声明后的那个看似多余的初始化器 {} 了——但你仍需在后面初始化所有的成员。

作为一个挑战,你能自行实现出 make_integer_sequence 吗?

答 1:

template <class T, T... Ints>
struct integer_sequence {};

template <class T>
struct integer_sequence_ns {
  template <T N, T... Ints>
  struct integer_sequence_helper {
    using type =
      typename integer_sequence_helper<
        N - 1, N - 1,
        Ints...>::type;
  };

  template <T... Ints>
  struct integer_sequence_helper<
    0, Ints...> {
    using type =
      integer_sequence<T, Ints...>;
  };
};

template <class T, T N>
using make_integer_sequence =
  typename integer_sequence_ns<T>::
    template integer_sequence_helper<
      N>::type;

如果一开始写成 template <class T, T N, T... Ints> struct integer_sequence_helper 的话,就会遇到错误“non-type template argument specializes a template parameter with dependent type ‘T’”(非类型的模板实参特化了一个使用依赖类型的‘T’的模板形参)。这是目前的 C++ 标准所不允许的写法,改写成嵌套类形式可以绕过这个问题。

答 2:

template <class T, T... Ints>
struct integer_sequence {};

template <class T, T N, T... Is>
auto make_integer_sequence_impl()
{
  if constexpr (N == 0) {
    return integer_sequence<
      T, Is...>();
  } else {
    return make_integer_sequence_impl<
      T, N - 1, N - 1, Is...>();
  }
}

template <class T, T N>
using make_integer_sequence =
  decltype(
    make_integer_sequence_impl<
      T, N>());

这又是一个 constexpr 能简化表达的例子。

第 19 讲

并发编程中哪些情况下会发生死锁?

答:多个线程里,如果没有或不能事先约定访问顺序,同时进行可阻塞的资源访问,访问顺序可以形成一个环,就会引发死锁。

可阻塞的资源访问可能包括(但不限于):

第 27 讲

你觉得 C++ REST SDK 的接口好用吗?如果好用,原因是什么?如果不好用,你有什么样的改进意见?

答:举几个可能的改进点。

C++ REST SDK 的 uri::decode 接口设计有不少问题:

换个说法,目前的接口能正确处理“/search?q=query%20string”这样的请求,但不能正确处理“/search?%71=query+string”这样的请求。

应当有一个 split_query_and_decode 接口,同时执行分割和解码。

另外,json 的接口也还是不够好用,最主要是没有使用初始化列表的构造。构造复杂的 JSON 结构有点啰嗦了。

fstream::open_ostream 缺省行为跟 std::ofstream 不一样应该是个 bug。应当要么修正接口(接口缺省参数里带上 trunc),要么修正实现(跟 std::ofstream 一样把 out 当成 out|trunc)。

第 28 讲

“概念”可以为开发具体带来哪些好处?反过来,负面的影响又可能会是什么?

答:对于代码严谨、具有形式化思维的人,“概念”是个福音,它不仅可以大量消除 SFINAE 的使用,还能以较为精确和形式化的形式在代码里写出对类型的要求,使得代码变得清晰、易读。

但反过来说,“概念”比鸭子类型更严格。在代码加上概念约束后,相关代码很可能需要修改才能满足概念的要求,即使之前在实际使用中可能已经完全没有问题。从迭代器的角度,实际使用中最小功能集是构造、可复制、*、前置 ++、与 sentinel 类型对象的 !=(单一形式)。而为了满足迭代器概念,则要额外确保满足以下各点:

以上就是今天的全部内容了,希望能对你有所帮助!如果你有更多问题,还是请你在留言区中提出,我会一一解答。