Назад к основам! Презентация Essentials of Modern C++ на CppCon обсудила различные варианты передачи параметров и сравнила их производительность с простотой написания/обучения. «Расширенный» вариант (обеспечивающий наилучшую производительность во всех протестированных случаях, но слишком сложный для написания большинством разработчиков) был идеальной пересылкой, в приведенном примере (PDF, стр. 28):
class employee {
std::string name_;
public:
template <class String,
class = std::enable_if_t<!std::is_same<std::decay_t<String>,
std::string>::value>>
void set_name(String &&name) noexcept(
std::is_nothrow_assignable<std::string &, String>::value) {
name_ = std::forward<String>(name);
}
};
В примере используется функция шаблона со ссылкой на пересылку, с параметром шаблона String
, ограниченным с помощью enable_if
. Однако ограничение кажется неверным: кажется, что этот метод можно использовать только в том случае, если тип String
не является std::string
, что не имеет смысла. Это означает, что этот элемент std::string
может быть установлен с использованием всего, кроме значения std::string
.
using namespace std::string_literals;
employee e;
e.set_name("Bob"s); // error
Одно объяснение, которое я рассматривал, заключалось в том, что это простая опечатка, и ограничение должно было быть std::is_same<std::decay_t<String>, std::string>::value
вместо !std::is_same<std::decay_t<String>, std::string>::value
. Однако это будет означать, что сеттер не работает, например, с const char *
, и очевидно, что он предназначен для работы с этим типом, учитывая, что это один из случаев, проверенных в презентации.
Мне кажется, что правильное ограничение больше похоже на:
template <class String,
class = std::enable_if_t<std::is_assignable<decltype((name_)),
String>::value>>
void set_name(String &&name) noexcept(
std::is_nothrow_assignable<decltype((name_)), String>::value) {
name_ = std::forward<String>(name);
}
позволяя использовать все, что может быть назначено члену, с сеттером.
Есть ли у меня правильное ограничение? Есть ли другие улучшения, которые можно сделать? Есть ли какое-то объяснение исходному ограничению, возможно, оно было вырвано из контекста?
Также мне интересно, действительно ли сложные, «необучаемые» части этой декларации так полезны. Поскольку мы не используем перегрузку, мы можем просто полагаться на обычное создание экземпляра шаблона:
template <class String>
void set_name(String &&name) noexcept(
std::is_nothrow_assignable<decltype((name_)), String>::value) {
name_ = std::forward<String>(name);
}
И, конечно, есть некоторые споры о том, действительно ли noexcept
имеет значение, некоторые говорят, что не стоит слишком беспокоиться об этом, за исключением примитивов перемещения/перестановки:
template <class String>
void set_name(String &&name) {
name_ = std::forward<String>(name);
}
Может быть, с концепциями было бы несложно ограничить шаблон просто для улучшения сообщений об ошибках.
template <class String>
requires std::is_assignable<decltype((name_)), String>::value
void set_name(String &&name) {
name_ = std::forward<String>(name);
}
У этого все еще есть недостатки, заключающиеся в том, что он не может быть виртуальным и что он должен быть в заголовке (хотя, надеюсь, модули в конечном итоге отобразят это спорным), но это кажется довольно обучаемым.