这是本章的问题解决部分。
编写一个表示 IPv4 地址的类。实现能够从控制台读写这些地址所需的功能。用户应该能够以虚线形式输入值,如127.0.0.1
或168.192.0.100
。这也是 IPv4 地址应该格式化为输出流的形式。
编写一个程序,允许用户输入代表一个范围的两个 IPv4 地址,并列出该范围内的所有地址。扩展为前面的问题定义的结构,以实现请求的功能。
编写一个表示二维数组容器的类模板,其中包含元素访问(at()
和data()
)、容量查询、迭代器、填充和交换的方法。应该可以移动这种类型的对象。
编写一个函数模板,可以取任意数量的参数,并返回所有参数的最小值,使用operator <
进行比较。写一个这个函数模板的变体,可以用二进制比较函数参数化来代替operator <
使用。
编写一个通用函数,可以在有方法push_back(T&& value)
的容器末尾添加任意数量的元素。
编写一组通用函数,用于检查给定容器中是否存在任何、所有或没有指定的参数。这些函数应该可以编写如下代码:
std::vector<int> v{ 1, 2, 3, 4, 5, 6 };
assert(contains_any(v, 0, 3, 30));
std::array<int, 6> a{ { 1, 2, 3, 4, 5, 6 } };
assert(contains_all(a, 1, 3, 5, 6));
std::list<int> l{ 1, 2, 3, 4, 5, 6 };
assert(!contains_none(l, 0, 6));
考虑一个操作系统句柄,如文件句柄。编写一个包装器来处理句柄的获取和释放,以及其他操作,例如验证句柄的有效性和将句柄所有权从一个对象转移到另一个对象。
写一个小的库,可以用三种最常用的刻度摄氏、华氏和开尔文来表示温度,并在它们之间转换。该库必须使您能够在所有这些刻度中写入温度文字,例如36.5_deg
代表摄氏度,97.7_f
代表华氏,309.65_K
代表开尔文;使用这些值执行操作;并在它们之间转换。
以下是上述问题解决部分的解决方案。
这个问题需要编写一个类来表示 IPv4 地址。这是一个 32 位的值,通常用十进制的点分格式表示,如168.192.0.100
*;*它的每个部分都是一个 8 位值,范围从 0 到 255。为了便于表示和处理,我们可以使用四个unsigned char
来存储地址值。这样的值可以由四个 T2 或一个 T3 构成。为了能够直接从控制台(或任何其他输入流)读取一个值,并且能够将该值写入控制台(或任何其他输出流),我们必须重载operator>>
和operator<<
。下面的清单显示了可以满足所请求功能的最小实现:
class ipv4
{
std::array<unsigned char, 4> data;
public:
constexpr ipv4() : data{ {0} } {}
constexpr ipv4(unsigned char const a, unsigned char const b,
unsigned char const c, unsigned char const d):
data{{a,b,c,d}} {}
explicit constexpr ipv4(unsigned long a) :
data{ { static_cast<unsigned char>((a >> 24) & 0xFF),
static_cast<unsigned char>((a >> 16) & 0xFF),
static_cast<unsigned char>((a >> 8) & 0xFF),
static_cast<unsigned char>(a & 0xFF) } } {}
ipv4(ipv4 const & other) noexcept : data(other.data) {}
ipv4& operator=(ipv4 const & other) noexcept
{
data = other.data;
return *this;
}
std::string to_string() const
{
std::stringstream sstr;
sstr << *this;
return sstr.str();
}
constexpr unsigned long to_ulong() const noexcept
{
return (static_cast<unsigned long>(data[0]) << 24) |
(static_cast<unsigned long>(data[1]) << 16) |
(static_cast<unsigned long>(data[2]) << 8) |
static_cast<unsigned long>(data[3]);
}
friend std::ostream& operator<<(std::ostream& os, const ipv4& a)
{
os << static_cast<int>(a.data[0]) << '.'
<< static_cast<int>(a.data[1]) << '.'
<< static_cast<int>(a.data[2]) << '.'
<< static_cast<int>(a.data[3]);
return os;
}
friend std::istream& operator>>(std::istream& is, ipv4& a)
{
char d1, d2, d3;
int b1, b2, b3, b4;
is >> b1 >> d1 >> b2 >> d2 >> b3 >> d3 >> b4;
if (d1 == '.' && d2 == '.' && d3 == '.')
a = ipv4(b1, b2, b3, b4);
else
is.setstate(std::ios_base::failbit);
return is;
}
};
ipv4
类可以如下使用:
int main()
{
ipv4 address(168, 192, 0, 1);
std::cout << address << std::endl;
ipv4 ip;
std::cout << ip << std::endl;
std::cin >> ip;
if(!std::cin.fail())
std::cout << ip << std::endl;
}
为了能够枚举给定范围内的 IPv4 地址,首先应该能够比较 IPv4 值。所以我们至少要实现operator<
,但是下面的列表包含了所有比较运算符的实现:==
、!=
、<
、>
、<=
和>=
。此外,为了增加 IPv4 值,提供了前缀和后缀operator++
的实现。以下代码是上一个问题的 IPv4 类的扩展:
ipv4& operator++()
{
*this = ipv4(1 + to_ulong());
return *this;
}
ipv4& operator++(int)
{
ipv4 result(*this);
++(*this);
return *this;
}
friend bool operator==(ipv4 const & a1, ipv4 const & a2) noexcept
{
return a1.data == a2.data;
}
friend bool operator!=(ipv4 const & a1, ipv4 const & a2) noexcept
{
return !(a1 == a2);
}
friend bool operator<(ipv4 const & a1, ipv4 const & a2) noexcept
{
return a1.to_ulong() < a2.to_ulong();
}
friend bool operator>(ipv4 const & a1, ipv4 const & a2) noexcept
{
return a2 < a1;
}
friend bool operator<=(ipv4 const & a1, ipv4 const & a2) noexcept
{
return !(a1 > a2);
}
friend bool operator>=(ipv4 const & a1, ipv4 const & a2) noexcept
{
return !(a1 < a2);
}
通过对前一个问题中的ipv4
类进行这些更改,我们可以编写以下程序:
int main()
{
std::cout << "input range: ";
ipv4 a1, a2;
std::cin >> a1 >> a2;
if (a2 > a1)
{
for (ipv4 a = a1; a <= a2; a++)
{
std::cout << a << std::endl;
}
}
else
{
std::cerr << "invalid range!" << std::endl;
}
}
在考虑如何定义这样的结构之前,让我们考虑几个测试用例。以下代码片段显示了请求的所有功能:
int main()
{
// element access
array2d<int, 2, 3> a {1, 2, 3, 4, 5, 6};
for (size_t i = 0; i < a.size(1); ++ i)
for (size_t j = 0; j < a.size(2); ++ j)
a(i, j) *= 2;
// iterating
std::copy(std::begin(a), std::end(a),
std::ostream_iterator<int>(std::cout, " "));
// filling
array2d<int, 2, 3> b;
b.fill(1);
// swapping
a.swap(b);
// moving
array2d<int, 2, 3> c(std::move(b));
}
注意,对于元素访问,我们使用的是operator()
,比如在a(i,j)
中,而不是operator[]
,比如在a[i][j]
中,因为只有前者可以接受多个参数(每个维度上的索引一个)。后者只能有一个参数,为了启用像a[i][j]
这样的表达式,它必须返回一个中间类型(一个基本上代表一行的类型),该中间类型反过来重载operator[]
以返回单个元素。
已经有存储固定或可变长度元素序列的标准容器。这个二维数组类应该只是这样一个容器的适配器。在std::array
和std::vector
之间进行选择时,我们应该考虑两件事:
array2d
类应该具有移动语义,以便能够移动对象- 应该可以列出初始化这种类型的对象
std::array
容器只有在它容纳的元素是可移动构造和可移动分配的情况下才是可移动的。另一方面,它不能由std::initializer_list
构成。因此,更可行的选择仍然是std::vector
。
在内部,这个适配器容器可以将其数据存储在向量的向量中(每行是一个带有C
元素的vector<T>
,而 2D 数组的R
这样的元素存储在一个vector<vector<T>>
中)或者类型为T
的R![](img/2f9ae4c1-380b-4377-84dd-a28429c062c5.png)C
元素的单个向量中。在后一种情况下,第i
行和第j
列的元素位于索引i * C + j
处。这种方法占用的内存更少,将所有数据存储在一个连续的块中,并且实现起来也更简单。由于这些原因,它是首选的解决方案。
具有所请求功能的二维数组类的可能实现如下所示:
template <class T, size_t R, size_t C>
class array2d
{
typedef T value_type;
typedef value_type* iterator;
typedef value_type const* const_iterator;
std::vector<T> arr;
public:
array2d() : arr(R*C) {}
explicit array2d(std::initializer_list<T> l):arr(l) {}
constexpr T* data() noexcept { return arr.data(); }
constexpr T const * data() const noexcept { return arr.data(); }
constexpr T& at(size_t const r, size_t const c)
{
return arr.at(r*C + c);
}
constexpr T const & at(size_t const r, size_t const c) const
{
return arr.at(r*C + c);
}
constexpr T& operator() (size_t const r, size_t const c)
{
return arr[r*C + c];
}
constexpr T const & operator() (size_t const r, size_t const c) const
{
return arr[r*C + c];
}
constexpr bool empty() const noexcept { return R == 0 || C == 0; }
constexpr size_t size(int const rank) const
{
if (rank == 1) return R;
else if (rank == 2) return C;
throw std::out_of_range("Rank is out of range!");
}
void fill(T const & value)
{
std::fill(std::begin(arr), std::end(arr), value);
}
void swap(array2d & other) noexcept { arr.swap(other.arr); }
const_iterator begin() const { return arr.data(); }
const_iterator end() const { return arr.data() + arr.size(); }
iterator begin() { return arr.data(); }
iterator end() { return arr.data() + arr.size(); }
};
可以使用可变函数模板编写可以接受可变数量参数的函数模板。为此,我们需要实现编译时递归(实际上只是通过一组重载函数的调用)。下面的代码片段显示了如何实现请求的函数:
template <typename T>
T minimum(T const a, T const b) { return a < b ? a : b; }
template <typename T1, typename... T>
T1 minimum(T1 a, T... args)
{
return minimum(a, minimum(args...));
}
int main()
{
auto x = minimum(5, 4, 2, 3);
}
为了能够使用用户提供的二进制比较函数,我们需要编写另一个函数模板。比较函数必须是第一个参数,因为它不能跟随函数参数包。另一方面,这不能是先前最小函数的重载,而是一个具有不同名称的函数。原因是编译器无法区分模板参数列表<typename T1, typename... T>
和<class Compare, typename T1, typename... T>
。改动很小,在这个片段中应该很容易理解:
template <class Compare, typename T>
T minimumc(Compare comp, T const a, T const b)
{ return comp(a, b) ? a : b; }
template <class Compare, typename T1, typename... T>
T1 minimumc(Compare comp, T1 a, T... args)
{
return minimumc(comp, a, minimumc(comp, args...));
}
int main()
{
auto y = minimumc(std::less<>(), 3, 2, 1, 0);
}
使用变量函数模板可以编写具有任意数量参数的函数。该函数应该将容器作为第一个参数,后跟一个可变数量的参数,这些参数表示要添加到容器后面的值。然而,使用 fold 表达式可以大大简化编写这样的函数模板。这里显示了这样一个实现:
template<typename C, typename... Args>
void push_back(C& c, Args&&... args)
{
(c.push_back(args), ...);
}
在下面的列表中可以看到使用这个函数模板和各种容器类型的例子:
int main()
{
std::vector<int> v;
push_back(v, 1, 2, 3, 4);
std::copy(std::begin(v), std::end(v),
std::ostream_iterator<int>(std::cout, " "));
std::list<int> l;
push_back(l, 1, 2, 3, 4);
std::copy(std::begin(l), std::end(l),
std::ostream_iterator<int>(std::cout, " "));
}
能够检查可变数量参数存在与否的要求表明,我们应该编写可变函数模板。然而,这些函数需要一个助手函数,一个检查容器中是否找到元素并返回一个bool
来指示成功或失败的通用函数。由于所有这些我们可以称为contains_all
、contains_any
和contains_none
的函数都是对辅助函数返回的结果应用逻辑运算符,因此我们将使用 fold 表达式来简化代码。在折叠表达式扩展后,短路评估被启用,这意味着我们只评估导致确定结果的元素。因此,如果我们正在寻找所有 1、2 和 3 的存在,并且缺少 2,则函数将在容器中查找值 2 后返回,而不检查值 3:
template<class C, class T>
bool contains(C const & c, T const & value)
{
return std::end(c) != std::find(std::begin(c), std::end(c), value);
}
template<class C, class... T>
bool contains_any(C const & c, T &&... value)
{
return (... || contains(c, value));
}
template<class C, class... T>
bool contains_all(C const & c, T &&... value)
{
return (... && contains(c, value));
}
template<class C, class... T>
bool contains_none(C const & c, T &&... value)
{
return !contains_any(c, std::forward<T>(value)...);
}
系统句柄是对系统资源的一种引用形式。因为所有的操作系统至少最初都是用 C 语言编写的,所以句柄的创建和释放是通过专用的系统函数来完成的。这增加了因错误处置(如在例外情况下)而导致资源泄漏的风险。在下面的代码片段中,针对 Windows,您可以看到一个打开、读取并最终关闭文件的函数。然而,这有两个问题:在一种情况下,开发人员在离开函数之前忘记关闭句柄;在另一种情况下,在句柄被正确关闭之前调用抛出的函数,而不会捕获异常。但是,由于函数抛出,清理代码永远不会执行:
void bad_handle_example()
{
bool condition1 = false;
bool condition2 = true;
HANDLE handle = CreateFile(L"sample.txt",
GENERIC_READ,
FILE_SHARE_READ,
nullptr,
OPEN_EXISTING,
FILE_ATTRIBUTE_NORMAL,
nullptr);
if (handle == INVALID_HANDLE_VALUE)
return;
if (condition1)
{
CloseHandle(handle);
return;
}
std::vector<char> buffer(1024);
unsigned long bytesRead = 0;
ReadFile(handle,
buffer.data(),
buffer.size(),
&bytesRead,
nullptr);
if (condition2)
{
// oops, forgot to close handle
return;
}
// throws exception; the next line will not execute
function_that_throws();
CloseHandle(handle);
}
C++ 包装器类可以确保在包装器对象超出范围并被销毁时(无论是通过正常执行路径还是作为异常的结果)正确处置句柄。正确的实现应该考虑不同类型的句柄,用一定范围的值来指示无效的句柄(如 0/null 或-1)。下面显示的实现提供了:
- 对象被破坏时句柄的显式获取和自动释放
- 移动语义以实现句柄所有权的转移
- 比较运算符检查两个对象是否引用同一个句柄
- 交换和重置等附加操作
The implementation shown here is a modified version of the handle class implemented by Kenny Kerr and published in the article Windows with C++ - C++ and the Windows API, MSDN Magazine, July 2011, https://msdn.microsoft.com/en-us/magazine/hh288076.aspx. Although the handle traits shown here refer to Windows handles, it should be fairly simple to write traits appropriate for other platforms.
template <typename Traits>
class unique_handle
{
using pointer = typename Traits::pointer;
pointer m_value;
public:
unique_handle(unique_handle const &) = delete;
unique_handle& operator=(unique_handle const &) = delete;
explicit unique_handle(pointer value = Traits::invalid()) noexcept
:m_value{ value }
{}
unique_handle(unique_handle && other) noexcept
: m_value{ other.release() }
{}
unique_handle& operator=(unique_handle && other) noexcept
{
if (this != &other)
reset(other.release());
return *this;
}
~unique_handle() noexcept
{
Traits::close(m_value);
}
explicit operator bool() const noexcept
{
return m_value != Traits::invalid();
}
pointer get() const noexcept { return m_value; }
pointer release() noexcept
{
auto value = m_value;
m_value = Traits::invalid();
return value;
}
bool reset(pointer value = Traits::invalid()) noexcept
{
if (m_value != value)
{
Traits::close(m_value);
m_value = value;
}
return static_cast<bool>(*this);
}
void swap(unique_handle<Traits> & other) noexcept
{
std::swap(m_value, other.m_value);
}
};
template <typename Traits>
void swap(unique_handle<Traits> & left, unique_handle<Traits> & right) noexcept
{
left.swap(right);
}
template <typename Traits>
bool operator==(unique_handle<Traits> const & left,
unique_handle<Traits> const & right) noexcept
{
return left.get() == right.get();
}
template <typename Traits>
bool operator!=(unique_handle<Traits> const & left,
unique_handle<Traits> const & right) noexcept
{
return left.get() != right.get();
}
struct null_handle_traits
{
using pointer = HANDLE;
static pointer invalid() noexcept { return nullptr; }
static void close(pointer value) noexcept
{
CloseHandle(value);
}
};
struct invalid_handle_traits
{
using pointer = HANDLE;
static pointer invalid() noexcept { return INVALID_HANDLE_VALUE; }
static void close(pointer value) noexcept
{
CloseHandle(value);
}
};
using null_handle = unique_handle<null_handle_traits>;
using invalid_handle = unique_handle<invalid_handle_traits>;
定义了这个句柄类型后,我们可以用更简单的术语重写前面的例子,避免所有那些由于异常发生而没有正确关闭句柄的问题,或者仅仅因为开发人员在不再需要时忘记释放资源。这段代码更简单、更健壮:
void good_handle_example()
{
bool condition1 = false;
bool condition2 = true;
invalid_handle handle{
CreateFile(L"sample.txt",
GENERIC_READ,
FILE_SHARE_READ,
nullptr,
OPEN_EXISTING,
FILE_ATTRIBUTE_NORMAL,
nullptr) };
if (!handle) return;
if (condition1) return;
std::vector<char> buffer(1024);
unsigned long bytesRead = 0;
ReadFile(handle.get(),
buffer.data(),
buffer.size(),
&bytesRead,
nullptr);
if (condition2) return;
function_that_throws();
}
为了满足这一要求,我们需要为几种类型、运算符和函数提供一个实现:
- 支持的温标计数称为
scale
。 - 表示温度值的类模板,用刻度参数化,称为
quantity
。 - 比较运算符
==
、!=
、<
、>
、<=
和>=
,它们比较同一时间的两个量。 - 加减相同数量类型值的算术运算符
+
和-
。另外,我们可以实现成员运营商+=
和-+
。 - 将温度从一个刻度转换到另一个刻度的函数模板,称为
temperature_cast
。这个函数本身不执行转换,而是使用类型特征来完成转换。 - 文字运算符
""_deg
、""_f
和""_k
,用于创建用户定义的温度文字。
For brevity, the following snippet only contains the code that handles Celsius and Fahrenheit temperatures. You should take it as a further exercise to extend the code with support for the Kelvin scale. The code accompanying the book contains the full implementation of all three required scales.
are_equal()
函数是一个用于比较浮点值的实用函数:
bool are_equal(double const d1, double const d2,
double const epsilon = 0.001)
{
return std::fabs(d1 - d2) < epsilon;
}
可能的温标的枚举和代表温度值的类别定义如下:
namespace temperature
{
enum class scale { celsius, fahrenheit, kelvin };
template <scale S>
class quantity
{
const double amount;
public:
constexpr explicit quantity(double const a) : amount(a) {}
explicit operator double() const { return amount; }
};
}
这里可以看到quantity<S>
类的比较运算符:
namespace temperature
{
template <scale S>
inline bool operator==(quantity<S> const & lhs, quantity<S> const & rhs)
{
return are_equal(static_cast<double>(lhs), static_cast<double>(rhs));
}
template <scale S>
inline bool operator!=(quantity<S> const & lhs, quantity<S> const & rhs)
{
return !(lhs == rhs);
}
template <scale S>
inline bool operator< (quantity<S> const & lhs, quantity<S> const & rhs)
{
return static_cast<double>(lhs) < static_cast<double>(rhs);
}
template <scale S>
inline bool operator> (quantity<S> const & lhs, quantity<S> const & rhs)
{
return rhs < lhs;
}
template <scale S>
inline bool operator<=(quantity<S> const & lhs, quantity<S> const & rhs)
{
return !(lhs > rhs);
}
template <scale S>
inline bool operator>=(quantity<S> const & lhs, quantity<S> const & rhs)
{
return !(lhs < rhs);
}
template <scale S>
constexpr quantity<S> operator+(quantity<S> const &q1,
quantity<S> const &q2)
{
return quantity<S>(static_cast<double>(q1) +
static_cast<double>(q2));
}
template <scale S>
constexpr quantity<S> operator-(quantity<S> const &q1,
quantity<S> const &q2)
{
return quantity<S>(static_cast<double>(q1) -
static_cast<double>(q2));
}
}
为了在不同尺度的温度值之间进行转换,我们将定义一个名为temperature_cast()
的函数模板,该模板利用几个类型特征来执行实际转换。所有这些都显示在这里,虽然不是所有的类型特征;其他的可以在本书附带的代码中找到:
namespace temperature
{
template <scale S, scale R>
struct conversion_traits
{
static double convert(double const value) = delete;
};
template <>
struct conversion_traits<scale::celsius, scale::fahrenheit>
{
static double convert(double const value)
{
return (value * 9) / 5 + 32;
}
};
template <>
struct conversion_traits<scale::fahrenheit, scale::celsius>
{
static double convert(double const value)
{
return (value - 32) * 5 / 9;
}
};
template <scale R, scale S>
constexpr quantity<R> temperature_cast(quantity<S> const q)
{
return quantity<R>(conversion_traits<S, R>::convert(
static_cast<double>(q)));
}
}
下面的代码片段显示了用于创建温度值的文字运算符。这些操作符被定义在一个单独的命名空间中,称为temperature_scale_literals
,这是一个很好的做法,以便将名称与其他文字操作符冲突的风险降至最低:
namespace temperature
{
namespace temperature_scale_literals
{
constexpr quantity<scale::celsius> operator "" _deg(
long double const amount)
{
return quantity<scale::celsius> {static_cast<double>(amount)};
}
constexpr quantity<scale::fahrenheit> operator "" _f(
long double const amount)
{
return quantity<scale::fahrenheit> {static_cast<double>(amount)};
}
}
}
以下示例显示了如何定义两个温度值,一个以摄氏度为单位,一个以华氏度为单位,并在两者之间进行转换:
int main()
{
using namespace temperature;
using namespace temperature_scale_literals;
auto t1{ 36.5_deg };
auto t2{ 79.0_f };
auto tf = temperature_cast<scale::fahrenheit>(t1);
auto tc = temperature_cast<scale::celsius>(tf);
assert(t1 == tc);
}