Hatred's Log Place

DON'T PANIC!

Apr 29, 2016 - 3 minute read - programming c++

std::string_view и временные объекты

Идея поста родилась при употреблении чая во внутрь на южной кухне.

Недавно смотрел один доклад (точнее бегло просматривал) про Rust и в момент, когда начался рассказ про life-time, глаз зацепился за такой опасный пример из мира C++:

string get_url()
{
  return "http://htrd.su";
}

string_view get_schema(string_view v)
{
  // тут какие-то действия, я их опущу
  auto result = v;
  return result;
}

int main()
{
  auto v = get_schema(get_url());
}

Что такое string_view - смотреть тут или тут. Если коротко - это невладеющая строка. Полезна для экономии на аллокациях, когда нужно работать с частями исходной строки.

В общем, из природы string_view следует и проблемы в коде выше: get_url() вернёт временный объект, который будет уничтожен в конце выражения, а следовательно, v будет ссылаться на невалидный участок памяти.

У меня в голове родилось, сходу, вариант защиты от такого: так как string_view не владеет строкой, то перемещение для строки сделать невозможно (да и семантически неверно), а перемещающий конструктор будет предпочтён для временного объекта. Следовательно если сделать перемещающий конструктор для string у string_view удалённым, то код выше сломается на этапе компиляции.

Подумав про это, решил глянуть, неужели разработчики стандарта об этом не подумали тоже… Оказалось нет:

Проверил концепт на простом примере:

struct string_view
{
	string_view() = default;
	string_view(const string_view& other) = default;
	
	string_view(const string& str)
		: ptr(str.c_str()), size(str.length())
	{}
	
	string_view(const char* str)
		: ptr(str), size(::strlen(str))
	{}
	
	string_view(const char* str, size_t len)
		: ptr(str), size(len)
	{}
	
	// Protection for temporary objects
	string_view(string&& str) = delete;
	
	string_view& operator=(const string_view&) = default; 
	
	const char *ptr = nullptr;
	size_t      size = 0;
};

Как видим, я запретил перемещение для std::string. И теперь код из примера выше не собирается с такой ошибкой:

prog.cpp: In function 'int main()':
prog.cpp:56:33: error: use of deleted function 'string_view::string_view(std::string&&)'
   auto v = get_schema(get_url());
                                 ^
prog.cpp:23:2: note: declared here
  string_view(string&& str) = delete;
  ^

Ошибочным является и тоже не соберётся такой код:

string_view v2;
v2 = get_url();

При этом прочие валидные сценарии продолжают работать:

// 1
string str = get_url();
auto v1 = get_schema(str);
// 2	
string_view v2 = "hello";
// 3	
auto v3 = get_schema("http://htrd.su");

Нельзя не сказать, что ищущий всегда найдёт способ обойти защиты и отстрелить ногу из гаубицы:

string_view get_schema2(string url)
{   
    string_view v(url);
    return v;
}

const char* get_schema3(string url)
{
    return url.c_str();
}

...

// WA for protection
auto        v1 = get_schema2(get_url());
auto        v2 = get_schema(get_url().c_str());
string_view v3 = get_schema3(get_url());

Такой код соберётся, но будет повторять ошибку из самого первого примера: использование уже освобождённой памяти. В общем: ищущий да обрящет.

Собственно у меня резонный вопрос: почему такая защита не сделана в Boost и экспериментальных реализация string_view в STL? Я, быть может, не вижу какой-то очевидной (или не очень) вещи, которая препятствует этому?

PS ну и полный листинг и ссылка на ideone:

#include <iostream>
#include <cstring>
using namespace std;

struct string_view
{
	string_view() = default;
	string_view(const string_view& other) = default;
	
	string_view(const string& str)
		: ptr(str.c_str()), size(str.length())
	{}
	
	string_view(const char* str)
		: ptr(str), size(::strlen(str))
	{}
	
	string_view(const char* str, size_t len)
		: ptr(str), size(len)
	{}
	
	// Protection for temporary objects
	string_view(string&& str) = delete;
	
	string_view& operator=(const string_view&) = default; 
	
	const char *ptr = nullptr;
	size_t      size = 0;
};

string get_url()
{
	return "http://htrd.su";
}

string_view get_schema(string_view uri)
{
	return uri;
}

string_view get_schema2(string url)
{
	string_view v(url);
	return v;
}

const char* get_schema3(string url)
{
	return url.c_str();
}

int main() 
{
	{
		// Compilation error:
		//auto v1 = get_schema(get_url());
		
		//string_view v2;
		//v2 = get_url();
	}
	
	{
		// WA for protection
		auto v1 = get_schema2(get_url());
		auto v2 = get_schema(get_url().c_str());
	}
	
	{
		// Valid usage	
		string str = get_url();
		auto v1 = get_schema(str);
		
		string_view v2 = "hello";
		
		auto v3 = get_schema("http://htrd.su");
	}
	
	
	
	return 0;
}