Подтвердить что ты не робот

Предоставление различных реализаций класса в зависимости от lvalue/rvalue при использовании шаблонов выражений

Проблема

Предположим, что мы реализуем класс string, который представляет, uhm, строки. Затем мы хотим добавить 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;

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

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>, т.е. Тип, создаваемый магией шаблона выражения для нас.

Теперь зачем использовать скрытый тип? Фактически, он не использует его явно - , но С++ 11 автоматически делает!

auto result = str1 + str2 + ... + strN; // guess what going on here?

Вопрос

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

Поэтому я бы очень хотел, чтобы метод обнаруживал, что я строю rvalue или lvalue, а предоставляет различные варианты реализации шаблона выражения в зависимости от независимо от того, построено ли значение rvalue (сохранить ссылки) или lvalue (сделать копии).

Существует ли установленный шаблон проектирования для обработки этой ситуации?

Единственное, что мне удалось выяснить во время моих исследований, было то, что

  • Можно перегрузить функции-члены в зависимости от this как lvalue или rvalue, т.е.

    class C {
        void f() &; 
        void f() &&; // called on temporaries
    }
    

    однако, похоже, я тоже не могу это сделать на конструкторах.

  • В С++ нельзя действительно "перегружать типы", т.е. предлагать несколько реализаций одного и того же типа, в зависимости от того, как тип будет использоваться (экземпляры, созданные как lvalues или rvalues).

4b9b3361

Ответ 1

Я начал это в комментарии, но для этого это было немного большим. Затем дайте ему ответ (хотя он действительно не отвечает на ваш вопрос).

Это известная проблема с auto. Например, здесь обсуждался Herb Sutter и более подробно Motti Lanzkron .

Как говорится, в комитете были обсуждения, чтобы добавить operator auto в С++ для решения этой проблемы. Идея была бы вместо (или в дополнение) предоставления

operator string() const;

как вы упомянули, можно было бы предоставить

string operator auto() const;

для использования в контекстах вывода типов. В этом случае

auto result = str1 + str2 + ... + strN;

не выводит тип result как "правильный тип", а тип string, потому что возвращается operator auto().

AFAICT это не произойдет в С++ 14. С++ 17 pehaps...

Ответ 2

Разработка комментария который я сделал для OP; Пример:

Это решает проблему присвоения либо объекту, либо привязку к ссылке, а затем преобразование в тип назначения. Это не исчерпывающее решение проблемы (см. Также Yakk ответ на мой комментарий), но он предотвращает сценарий, представленный в OP, и делает его более сложным для написания такого типа подверженного ошибкам кода.

Изменить: Возможно, не удастся расширить этот подход для шаблонов классов (более конкретно, специализация std::move). Macro'ing может работать для этой конкретной проблемы, но явно уродлив. Перегрузка 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{};
}

Ответ 3

Просто дикая идея (не пробовал):

template<class T, class U>
class stringbuilder
{
  stringbuilder(stringbuilder const &) = delete;
}

не приведет к ошибке компиляции?

Ответ 4

Возможным подходом будет использование шаблона нулевого объекта. Хотя это может привести к тому, что построитель строк будет больше, он все равно избежит выделения памяти.

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, вы сохраняете ссылку на реальный объект. Если аргументом конструктора является rvalue, вы можете переместить его во внутреннюю переменную почти без затрат (операции перемещения являются дешевыми) и сохранить ссылку на этот внутренний объект. Остальная часть кода может получить доступ к справочному знанию (ну, по крайней мере, надеясь), что строка все равно будет жива.

Предполагаемая часть состоит в том, что нет ничего, что блокирует неправильное использование, если lvalue будет передано, но объект будет уничтожен до того, как stringbuilder завершит свое задание.

Ответ 5

Вот еще одна попытка решить проблему оборванных ссылок. Он не решает проблему ссылок на вещи, которые были изменены.

Идея состоит в том, чтобы хранить временные значения в значениях, но иметь ссылки на 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: Я использовал С++ 14 только для упрощения типов типов.