Сделайте настраиваемый тип "связанный" (совместимый с std:: tie)

У меня есть пользовательский тип (который я могу продлить):

struct Foo {
    int a;
    string b;
};

Как я могу создать экземпляр этого объекта, который можно присваивать std::tie, т.е. std::tuple ссылок?

Foo foo = ...;

int a;
string b;

std::tie(a, b) = foo;

Неудачные попытки:

Перегрузка оператора присваивания для tuple<int&,string&> = Foo невозможна, поскольку оператор присваивания является одним из двоичных операторов, которые должны быть членами объекта левой стороны.

Поэтому я попытался решить эту проблему, выполнив подходящий оператор преобразования tuple. Не удалось выполнить следующие версии:

  • operator tuple<int,string>() const
  • operator tuple<const int&,const string&>() const

Они приводят к ошибке при присваивании, говоря, что "operator = не перегружается для tuple<int&,string&> = Foo". Я думаю, это связано с тем, что "преобразование в любой шаблон X + параметр шаблона X для оператора =" не работает вместе, только один из них сразу.

Несовершенная попытка:

Следовательно, я попытался реализовать оператор преобразования для точного типа привязки:

Назначение теперь работает, поскольку типы теперь (после преобразования) точно такие же, но это не будет работать для трех сценариев, которые я хотел бы поддержать:

  • Если связь связана с переменными различных, но конвертируемых типов (т.е. измените int a; на long long a; на стороне клиента), она терпит неудачу, поскольку типы должны полностью соответствовать. Это противоречит обычному использованию назначения кортежа в кортеж ссылок, который позволяет конвертировать типы. (1)
  • Оператор преобразования должен вернуть связь, для которой должны быть заданы ссылки lvalue. Это не будет работать для временных значений или константных членов. (2)
  • Если оператор преобразования не const, присваивание также не выполняется для const Foo с правой стороны. Чтобы реализовать const-версию преобразования, нам нужно снять константу членов объекта const. Это некрасиво и может быть злоупотреблено, что приводит к поведению undefined.

Я вижу только альтернативу в предоставлении моей собственной функции tie function + class вместе с моими "связанными с привязкой" объектами, что заставляет меня дублировать функции std::tie, которые мне не нравятся (не то, что я затрудниться с этим, но ему нехорошо это делать).

Я думаю, что в конце дня вывод заключается в том, что это один из недостатков реализации библиотеки только для кортежей. Они не такие волшебные, как нам бы хотелось.

EDIT:

Как оказалось, не существует реального решения всех вышеперечисленных проблем. Очень хороший ответ объяснил бы, почему это не разрешимо. В частности, я хотел бы, чтобы кто-то пролил свет на то, почему "неудачные попытки" не могут работать.


(1): ужасный взлом - это записать преобразование в качестве шаблона и преобразовать в запрошенные типы членов в операторе преобразования. Это ужасный взлом, потому что я не знаю, где хранить эти конвертированные элементы. В эта демонстрация Я использую статические переменные, но это не поточно-реентерабельный.

(2): Можно применить такой же взлом, как и в (1).

Ответ 1

Почему текущие попытки не срабатывают

std::tie(a, b) создает a std::tuple<int&, string&>. Этот тип не связан с std::tuple<int, string> и т.д.

std::tuple<T...> имеют несколько операторов присваивания:

  • Оператор присваивания по умолчанию, который принимает std::tuple<T...>
  • Преобразование шаблона-оператор-преобразование кортежа с пакетом параметров типа U..., который принимает std::tuple<U...>
  • Пара-преобразование шаблона присваивания-оператора с двумя параметрами типа U1, U2, который принимает std::pair<U1, U2>

Для этих трех версий существуют варианты копирования и перемещения; добавьте либо теги const&, либо && к типам, которые они принимают.

Шаблоны-операторы-назначения должны вывести свои аргументы шаблона из типа аргумента функции (т.е. типа RHS выражения-назначения).

Без оператора преобразования в Foo ни один из этих операторов присваивания не является жизнеспособным для std::tie(a,b) = foo. Если вы добавите оператор преобразования в Foo, то становится доступным только оператор присваивания по умолчанию: Вычисление типа шаблона не учитывает пользовательские преобразования. То есть вы не можете вывести аргументы шаблона для шаблонов присваивания-оператора из типа Foo.

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

Чтобы поддерживать преобразования типов элементов (например, назначение Foo::a в long), оператор преобразования Foo должен быть шаблоном:

struct Foo {
    int a;
    string b;
    template<typename T, typename U>
    operator std::tuple<T, U>();
};

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

Ответ 2

Есть только два способа, которыми вы можете попробовать:

  • Использование шаблонных операторов присваивания:
    Вам нужно публично выводить из типа, который точно соответствует шаблону присваивания-оператора.
  • Использовать нестандартные операторы присваивания:
    Предложите преобразование не explicit в тип, который ожидает не-шаблонный оператор копирования, поэтому он будет использоваться.
  • Третий вариант отсутствует.

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

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

struct X : tuple<int,int> {
};

struct Y {
    int i;
    operator tuple<int&,int&>() {return tuple<int&,int&>{i,i};}
};

int main()
{
    int a, b;
    tie(a, b) = make_tuple(9,9);
    tie(a, b) = X{};
    tie(a, b) = Y{};
    cout << a << ' ' << b << '\n';
}

На coliru: http://coliru.stacked-crooked.com/a/315d4a43c62eec8d

Ответ 3

Как уже объясняют другие ответы, вам либо нужно наследовать от tuple (чтобы соответствовать шаблону оператора присваивания), либо преобразовать в тот же самый tuple ссылок (чтобы соответствовать не templated оператор присваивания, принимающий tuple ссылок одного и того же типа).

Если вы наследуете кортеж, вы потеряете именованные члены, т.е. foo.a больше не возможно.

В этом ответе я предлагаю еще один вариант: если вы готовы заплатить за накладные расходы (постоянный на члена), вы можете одновременно иметь именованные члены, а также наследование кортежей, наследуя от кортежа ссылок const, т.е. константа самого объекта:

struct Foo : tuple<const int&, const string&> {
    int a;
    string b;

    Foo(int a, string b) :
        tuple{std::tie(this->a, this->b)},
        a{a}, b{b}
    {}
};

Эта "привязанная связь" позволяет назначить (не const!) Foo привязку типов конвертируемых компонентов. Так как "прикрепленная привязка" представляет собой набор ссылок, она автоматически присваивает текущие значения членам, хотя вы инициализировали его в конструкторе.

Почему "привязанный галстук" const? Поскольку в противном случае a const Foo можно было бы модифицировать с помощью прикрепленной привязки.

Пример использования с неточными составными типами привязки (обратите внимание на long long vs int):

int main()
{
    Foo foo(0, "bar");
    foo.a = 42;

    long long a;
    string b;

    tie(a, b) = foo;
    cout << a << ' ' << b << '\n';
}

напечатает

42 bar

Живая демонстрация

Таким образом, это решает проблемы 1. + 3., введя пространственные служебные данные.

Ответ 4

Этот вид делает то, что вы хотите правильно? (предполагается, что ваши значения могут быть связаны с типами курса...)

#include <tuple>
#include <string>
#include <iostream>
#include <functional>
using namespace std;


struct Foo {
    int a;
    string b;

    template <template<typename ...Args> class tuple, typename ...Args>
    operator tuple<Args...>() const {
        return forward_as_tuple(get<Args>()...);
    }

    template <template<typename ...Args> class tuple, typename ...Args>
    operator tuple<Args...>() {
        return forward_as_tuple(get<Args>()...);
    }

    private:
    // This is hacky, may be there is a way to avoid it...
    template <typename T>
    T get()
    { static typename remove_reference<T>::type i; return i; }

    template <typename T>
    T get() const
    { static typename remove_reference<T>::type i; return i; }

};

template <>
int&
Foo::get()
{ return a; }

template <>
string&
Foo::get()
{ return b; }

template <>
int&
Foo::get() const
{ return *const_cast<int*>(&a); }

template <>
string&
Foo::get() const
{ return *const_cast<string*>(&b); }

int main() {
    Foo foo { 42, "bar" };
    const Foo foo2 { 43, "gah" };

    int a;
    string b;

    tie(a, b) = foo;
    cout << a << ", " << b << endl;

    tie(a, b) = foo2;
    cout << a << ", " << b << endl;

}

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

Во-вторых, оператор преобразования не является явным, он будет преобразован в любой запрошенный тип кортежа (возможно, вы этого не хотите).

Основное преимущество заключается в том, что вам не нужно явно указывать тип преобразования, все выведенное...