С++17 подарил нам тип std::string_view
, призванный убить сразу двух зайцев:
- Проблемы с перегрузками для функций, которые должны работать хорошо как с C, так и с C++ строками
- А также программистов, периодически забывающих о правилах продления жизни временным объектам.
И так, проблема: функция хочет считать количество вхождений какого-то символа в строку:
int count_char(const std::string& s, char c) {
....
}
count_char("hello world", 'l'); // создастся временный объект std::string,
// выделится память, скопируется строка, а потом строка умрет и память
// деаллоцируется — плохо, много лишних операций
Так что нам нужна перегрузка для С-строк
int count_char(const char* s, char c) {
// мы тут не знаем ничего про длину строки
// она вообще null-териминированная?
// Можем только написать код, наивно рассчитывающий, что его
// будут вызывать правильно.
...
}
И будем либо дублировать код, немного адаптируя его под C-строки, либо сделаем функцию
int count_char_impl(const char* s, size_t len, char c) {
...
}
В которую поместим весь дублирующийся код и вызовем ее из перегрузок:
int count_char(const std::string& s, char c) {
return count_char_impl(s.data(), s.size(), c);
}
int count_char(const char* s, char c) {
return count_char_impl(s, strlen(s), c);
}
И тут на помощь приходит string_view
, как раз таки являющийся парой: указатель и размер. И убивает обе перегрузки:
int count_char(std::string_view s, char c) {
...
}
И все здорово, хорошо и замечательно, кроме одного но:
std::string_view
по сути является ссылочным типом, как const&
, и его можно конструировать из временных значений. Но, в отличие от просто const&
, никакого продления жизни не будет. Вернее будет, но не там, где ожидается.
auto GetString = []() -> std::string { return "hello"; };
std::string_view sv = GetString();
std::cout << sv << "\n"; // dangling reference!
В этом примере мы, конечно, почти явно стреляем себе в голову. Можно сделать стрельбу менее явной:
std::string_view common_prefix(std::string_view a, std::string_view b) {
auto len = std::min(a.size(), b.size());
auto common_count = [&]{
for (size_t common_len = 0; common_len < len; ++common_len) {
if (a[common_len] != b[common_len]) {
return common_len;
}
}
return len;
}();
return a.substr(0, common_count);
}
int main() {
using namespace std::string_literals;
{
auto common = common_prefix("helloW",
"hello"s + "World111111111111111111111");
std::cout << common << "\n"; // ok
}
{
auto common = common_prefix("hello"s + "World111111111111111111111111",
"helloW");
std::cout << common << "\n"; // dangling ref
}
}
Ситуация такая же, как с ранее рассмотренным std::min
.
Только защититься от такой функции common_prefix
, обернув ее в шаблон с помощью анализа rvalue/lvalue,
намного сложнее: нам нужно разобрать случаи const char*
и std::string
для каждого аргумента — в общем, все то, от чего нас введение std::string_view
«избавило».
Влететь в string_view
можно еще изящнее:
struct Person {
std::string name;
std::string_view Initials() const {
if (name.length() <= 2) {
return name;
}
return name.substr(0, 2); // copy — dangling reference!
}
};
Причем видно, что Clang хотя бы выдает предупреждение. А gcc — нет.
Все потому что std::string_view
настолько легендарный, что в clang сделали хоть какой-то lifetime checker сперва для него.