用于初始化 Nicolai Josuttis 的 c++17 演示文稿中的"customer class"模板



我刚刚看了Nicolai Josuttis的演讲《琐碎类移动语义的噩梦》

在演示中,他展示了如何构建一个"完美的客户类",以便它的构造函数接受最多三个参数,并尽可能少地使用malloc。以下是他提出的解决方案:

// 11 mallocs (4cr + 7cp + 1mv)
Customer(std::string f = "", std::string l = "", int i = 0) :
first(f), last(l), id(i) 
{}
// 5 mallocs (4cr + 1cp + 5mv)
Customer(std::string f = "", std::string l = "", int i = 0) :
first(std::move(f)), last(std::move(l)), id(i) 
{}
// all manual combinations carefully avoiding ambiguities, such as
// 5 mallocs (4cr + 1cp + 1mv)
Customer(const std::string&, const std::string&, int i = 0);
// 5 mallocs (4cr + 1cp + 1mv)
template<typename S1, typename S2 = std::string, typename = std::enable_if<std::is_convertible_v<std::string>>
Customer(S1&& f, S2&& l = "", int i = 0):
first(std::forward<S1>(f)), last(std::forward<S1>(l)), id(i)
{}

在这种情况下,我使用了一个成语(如下所示),他在演示中没有考虑到,我也没有在其他地方找到它,但我认为它在性能和用法方面都是合适的。它使用具有成员的结构体的私有继承。构造函数模板使用参数包来完成这项工作。所以我想知道:这种方法有什么问题吗?我是否应该预期在性能或使用方面出现一些问题?还有其他陷阱吗?这是一个我刚刚错过的俗语吗?(但为什么没有呢?)

下面是类的代码:
#include <string>
#include <utility>
struct CustomerData
{
std::string first;
std::string last;
int id;
};
class Customer: private CustomerData
{
public:
// Constructor template
template<typename... Args>
Customer(Args... args):
CustomerData{std::move(args)...}
{
}
};

编辑:1) POD更改为struct (POD包含字符串不是POD,感谢Daniel Langr的评论),2)如果不想观看整个视频,则添加解释性句子

编辑2:std::forward更改为std::move,感谢Jarod42的注释

编辑3:根据apple的要求添加参考代码apple

template <typename Args>
Customer(Args... args) :
CustomerData{std::move(args)...}
{
}

相当
Customer(std::string first, std::string last = "", int id = -1) :
first(std::move(first)), last(std::move(last)), id(id)
{}

(这是视频中首选的变体,但不是最有效的,因为有一些(最多4)额外的移动构造函数)

默认值(这也是视频中的一个关注点)可以直接在数据结构中处理。

性能相同。

你的版本更少啰嗦,但也更少自我说明。

对于大多数模板(尤其是转发引用),它不允许initializer_list(视频中没有使用/显示的测试用例):

Customer c({42, 'a'}, {it1, it2}, 51);

你的版本不是SFINAE友好的:std::is_constructible_v<Customer, float, std::vector<int>>会给出错误的答案。

同样,视频中关于继承的例子:

Vip vip{/*..*/}; 
Customer c(vip); // Would select wrongly you constructor

你可以添加SFINAE/requires来处理这些情况。

我会比较另一个选择

template <typename... Args>
requires(is_aggregate_constructible_v<CustomerData, Args&&...>)
Customer(Args&&... args) : CustomerData{std::forward<Args>(args)...}
{
}

比较
template <typename S1, typename S2 = std::string>
requires (std::is_constructible_v<std::string, S1&&>
&& std::is_constructible_v<std::string, S2&&>)
Customer(S1&& first, S2&& last = "", int id = -1) :
first(std::forward<S1>(first)),
second(std::forward<S2>(second)),
id(id),
{}

最有效的选择。

,

  • 相同性能
  • 更少的冗长和潜在的更少的自我文档。

在这里,两个版本都是模板,并且都存在构造初始化列表的问题。

总之,没有requires的版本有一些缺陷。

如果您添加了requires,那么转到建议的替代方案(转发引用而不是按值转发)将更有效。

当我使用"日志字符串"扩展示例时

#include <iostream>
#include <string>
#include <utility>
class CustomString {
public:
CustomString(const char *) {
std::cout << "CustomString(const char*)n"; }
CustomString(const std::string &) {
std::cout << "CustomString(const std::string&)n"; }
CustomString(std::string &&) {
std::cout << "CustomString(std::string&&)n"; }
CustomString(const CustomString &) {
std::cout << "CustomString(const CustomString&)n"; }
CustomString(CustomString &&) {
std::cout << "CustomString(CustomString&&)n"; }
};
struct CustomerData
{
CustomString first;
CustomString last;
int id;
};
class Customer: private CustomerData
{
public:
// Constructor template
template<typename... Args>
Customer(Args... args):
CustomerData{std::move(args)...}
{
}
};
int main()
{
std::cout << "John Doen";
Customer c{"John", "Doe", 1};
std::cout << "Janen";
CustomString jane{"Jane"};
std::cout << "Jane Doen";
Customer d{jane, "Doe", 1};
std::cout << "move Jane Doen";
Customer e{std::move(jane), "Doe", 1};
}

它给出如下输出

John Doe
CustomString(const char*)
CustomString(const char*)
Jane
CustomString(const char*)
Jane Doe
CustomString(const CustomString&)
CustomString(CustomString&&)
CustomString(const char*)
move Jane Doe
CustomString(CustomString&&)
CustomString(CustomString&&)
CustomString(const char*)

在第二个和第三个例子中,你可以看到一个额外的动作,这在演讲的版本中没有发生。这可能会或可能不会产生(小)差异。

最新更新