Сделайте настраиваемый тип "связанный" (совместимый с 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 для оператора =" не работает вместе, только один из них сразу.
Несовершенная попытка:
Следовательно, я попытался реализовать оператор преобразования для точного типа привязки:
-
operator tuple<int&,string&>() const
Демо
-
operator tuple<int&,string&>()
Демо
Назначение теперь работает, поскольку типы теперь (после преобразования) точно такие же, но это не будет работать для трех сценариев, которые я хотел бы поддержать:
- Если связь связана с переменными различных, но конвертируемых типов (т.е. измените
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;
}
Основной недостаток заключается в том, что к каждому члену можно обращаться только по их типам, теперь вы можете обойти это с помощью какого-либо другого механизма (например, определить тип для каждого члена и обернуть ссылку на тип по типу участника вы хотите получить доступ..)
Во-вторых, оператор преобразования не является явным, он будет преобразован в любой запрошенный тип кортежа (возможно, вы этого не хотите).
Основное преимущество заключается в том, что вам не нужно явно указывать тип преобразования, все выведенное...