Предположим, мы реализуем string
класс, который представляет собой строки Затем мы хотим добавить operator+
который соединяет два string
s, и решили реализовать это через шаблоны выражений чтобы избежать многократного распределения при выполнении str1 + str2 + ... + strN
,
Оператор будет выглядеть так:
stringbuilder<string, string> operator+(const string &a, const string &b)
stringbuilder
класс шаблона, который в свою очередь перегружает operator+
и имеет неявное string
оператор преобразования. Практически стандартное учебное упражнение:
template<class T, class U> class stringbuilder;
template<> class stringbuilder<string, string> {
stringbuilder(const string &a, const string &b) : a(a), b(b) {};
const string &a;
const string &b;
operator string() const;
// ...
}
// recursive case similar,
// building a stringbuilder<stringbuilder<...>, string>
Вышеприведенная реализация работает отлично, пока кто-то делает
string result = str1 + str2 + ... + strN;
Тем не мение, в этом есть небольшая ошибка. Присвоение результата переменной право type заставит эту переменную содержать ссылки на все строки, составляющие выражение. Это означает, например, что изменение одной из строк изменит результат:
void print(string);
string str1 = "foo";
string str2 = "bar";
right_type result = str1 + str2;
str1 = "fie";
print(result);
Это напечатает fiebar, из-за ссылки str1, хранящейся в шаблоне выражения. Становится хуже:
string f();
right_type result = str1 + f();
print(result); // kaboom
Теперь шаблон выражения будет содержать ссылку на уничтоженное значение, сбой вашей программы сразу.
Теперь, что это right_type
? Это конечно stringbuilder<stringbuilder<...>, string>
то есть тип, который генерирует для нас шаблонное выражение.
Теперь, почему можно использовать такой скрытый тип? Фактически, никто не использует это явно — но в C ++ 11 работает автоматически!
auto result = str1 + str2 + ... + strN; // guess what's going on here?
Суть в следующем: кажется, что это способ реализации шаблонов выражений (путем сохранения дешево ссылки вместо копирования значений или использования общих указателей) ломаются, как только кто-то пытается хранить сам шаблон выражения.
Поэтому я бы очень хотел обнаружение, если я строю Rvalue или Lvalue, а также обеспечить различные реализации шаблона выражения в зависимости от того, построено ли значение r (сохранить ссылки) или построено lvalue (создавать копии).
Существует ли установленный шаблон дизайна для решения этой ситуации?
Единственное, что мне удалось выяснить во время моего исследования, это то, что
Можно перегружать функции-члены в зависимости от this
быть lvalue или rvalue, т.е.
class C {
void f() &;
void f() &&; // called on temporaries
}
однако, кажется, я не могу сделать это и на конструкторах.
В C ++ никто не может сделать « тип перегрузки », то есть предлагают несколько реализаций одного и того же типа, в зависимости от того, каким будет тип используемый (экземпляры, созданные как lvalues или rvalues).
Я начал это в комментарии, но это было немного большим для этого. Тогда давайте сделаем это ответом (даже если он не отвечает на ваш вопрос).
Это известная проблема с auto
, Например, это обсуждалось Хербом Саттером Вот и более подробно Мотти Ланцкрон Вот.
Как говорится, были обсуждения в комитете, чтобы добавить operator auto
в C ++ для решения этой проблемы. Идея будет вместо (или в дополнение к) предоставления
operator string() const;
как вы упомянули, можно
string operator auto() const;
для использования в контекстах вывода типа. В этом случае,
auto result = str1 + str2 + ... + strN;
не будет выводить тип result
быть «правильным типом», а скорее типом string
потому что это то, что operator auto()
возвращается.
AFAICT это не произойдет в C ++ 14. C ++ 17 pehaps …
Разработка на комментарий, который я сделал к OP; пример:
Это только решает проблему присвоения объекта или привязки к ссылке и последующего преобразования в тип назначения. Это не комплексное решение проблемы (см. Также Yakk«s ответ на мой комментарий), но он предотвращает сценарий, представленный в OP, и затрудняет написание такого рода подверженного ошибкам кода.
Изменить: Возможно, не удастся расширить этот подход для шаблонов классов (более конкретно, специализация std::move
). Макроинг может работать для этой конкретной проблемы, но, очевидно, безобразно. перегрузка std::move
будет полагаться на UB.
#include <utility>
#include <cassert>
// your stringbuilder class
struct wup
{
// only use member functions with rvalue-ref-qualifier
// this way, no lvalues of this class can be used
operator int() &&
{
return 42;
}
};
// specialize `std::move` to "prevent" from converting lvalues to rvalue refs
// (make it much harder and more explicit)
namespace std
{
template<> wup&& move(wup&) noexcept
{
assert(false && "Do not use `auto` with this expression!");
}
// alternatively: no function body -> linker error
}
int main()
{
auto obj = wup{};
auto& lref = obj;
auto const& clref = wup{};
auto&& rref = wup{};
// fail because of conversion operator
int iObj = obj;
int iLref = lref;
int iClref = clref;
int iRref = rref;
int iClref_mv = std::move(clref);
// assert because of move specialization
int iObj_mv = std::move(obj);
int iLref_mv = std::move(lref);
int iRref_mv = std::move(rref);
// works
int i = wup{};
}
Просто дикая идея (еще не пробовал):
template<class T, class U>
class stringbuilder
{
stringbuilder(stringbuilder const &) = delete;
}
не приведет к ошибке компиляции?
Возможным подходом будет использование шаблона нулевого объекта. Хотя это может сделать ваш построитель строк больше, он все равно будет избегать выделения памяти.
template <>
class stringbuilder<std::string,std::string> {
std::string lhs_value;
std::string rhs_value;
const std::string& lhs;
const std::string& rhs;
stringbuilder(const std::string &lhs, const std::string &rhs)
: lhs(lhs), rhs(rhs) {}
stringbuilder(std::string&& lhs, const std::string &rhs)
: lhs_value(std::move(lhs)), lhs(lhs_value), rhs(rhs) {}
stringbuilder(const std::string& lhs, std::string&& rhs)
: rhs_value(std::move(rhs)), lhs(lhs), rhs(rhs_value) {}
stringbuilder(std::string&& lhs, std::string&& rhs)
: lhs_value(std::move(lhs)), rhs_value(std::move(rhs)),
lhs(lhs_value), rhs(rhs_value) {}
//...
Если аргумент конструктора является lvalue, тогда вы сохраняете ссылку на реальный объект. Если аргумент конструктора является r-значением, вы можете переместить его во внутреннюю переменную практически без затрат (операции перемещения дешевы) и сохранить ссылку на этот внутренний объект. Остальная часть кода может получить доступ к справке, зная (ну, по крайней мере, надеясь) что строка все еще будет жива.
надеясь Это связано с тем, что нет ничего, что блокировало бы неправильное использование, если передано lvalue, но объект уничтожен до того, как string Builder завершит свою работу.
Вот еще одна попытка решить проблему висячих ссылок. Это не решает проблему ссылок на вещи, которые изменены, хотя.
Идея состоит в том, чтобы хранить временные значения в значениях, но иметь ссылки на lvalues (что мы можем ожидать, чтобы продолжать жить после ;
).
// Temporary => store a copy
// Otherwise, store a reference
template <typename T>
using URefUnlessTemporary_t
= std::conditional_t<std::is_rvalue_reference<T&&>::value
, std::decay_t<T>
, T&&>
;
template <typename LHS, typename RHS>
struct StringExpression
{
StringExpression(StringExpression const&) = delete;
StringExpression(StringExpression &&) = default;
constexpr StringExpression(LHS && lhs_, RHS && rhs_)
: lhs(std::forward<LHS>(lhs_))
, rhs(std::forward<RHS>(rhs_))
{ }
explicit operator std::string() const
{
auto const len = size(*this);
std::string res;
res.reserve(len);
append(res, *this);
return res;
}
friend constexpr std::size_t size(StringExpression const& se)
{
return size(se.lhs) + size(se.rhs);
}friend void append(std::string & s, StringExpression const& se)
{
append(s, se.lhs);
append(s, se.rhs);
}
friend std::ostream & operator<<(std::ostream & os, const StringExpression & se)
{ return os << se.lhs << se.rhs; }
private:
URefUnlessTemporary_t<LHS> lhs;
URefUnlessTemporary_t<RHS> rhs;
};
template <typename LHS, typename RHS>
StringExpression<LHS&&,RHS&&> operator+(LHS && lhs, RHS && rhs)
{
return StringExpression<LHS&&,RHS&&>{std::forward<LHS>(lhs), std::forward<RHS>(rhs) };
}
Я не сомневаюсь, что это может быть упрощено.
int main ()
{
constexpr static auto c = exp::concatenator{};
{
std::cout << "RVREF\n";
auto r = c + f() + "toto";
std::cout << r << "\n";
std::string s (r);
std::cout << s << "\n";
}
{
std::cout << "\n\nLVREF\n";
std::string str="lvref";
auto r = c + str + "toto";
std::cout << r << "\n";
std::string s (r);
std::cout << s << "\n";
}
{
std::cout << "\n\nCLVREF\n";
std::string const str="clvref";
auto r = c + str + "toto";
std::cout << r << "\n";
std::string s (r);
std::cout << s << "\n";
}
}
NB: я не предоставляю size()
, append()
ни concatenator
они не являются точками, в которых лежат трудности.
PS: я использовал C ++ 14 только для упрощения черт типа.