使自定义类型"绑定"(与std :: tie兼容)

lee*_*mes 50 c++ tuples std c++11

考虑我有一个自定义类型(我可以扩展):

struct Foo {
    int a;
    string b;
};
Run Code Online (Sandbox Code Playgroud)

如何将此对象的实例分配给a std::tie,即std::tuple引用?

Foo foo = ...;

int a;
string b;

std::tie(a, b) = foo;
Run Code Online (Sandbox Code Playgroud)

尝试失败:

tuple<int&,string&> = Foo由于赋值运算符是必须是左侧对象成员的二元运算符之一,因此无法重载赋值运算符.

所以我试图通过实现一个合适的元组转换运算符来解决这个问题.以下版本失败:

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

它们导致赋值错误,告诉" operator =没有超载tuple<int&,string&> = Foo".我想这是因为"转换为任何类型的X +推导模板参数X for operator ="不能同时工作,只能同时使用其中一个.

不完美的尝试:

因此,我尝试为绑定确切类型实现转换运算符:

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

现在分配工作,因为类型现在(转换后)完全相同,但这不适用于我想支持的三种情况:

  1. 如果tie有不同但可转换类型的变量绑定(即更改int a;long long a;客户端),则失败,因为类型必须完全匹配.这与将元组分配给允许可转换类型的引用元组的通常用法相矛盾.(1)
  2. 转换运算符需要返回一个必须给出左值引用的平局.这不适用于临时值或const成员.(2)
  3. 如果转换运算符不是const,const Foo则右侧的赋值也会失败.要实现转换的const版本,我们需要破坏const主题成员的常量.这很丑陋,可能会被滥用,导致行为不确定.

我只看到一个替代方案,提供我自己的tie函数+类和我的"绑定"对象,这使我强制复制std::tie我不喜欢的功能(不是我觉得很难这样做,但它我不得不这样做.)

我认为在一天结束时,结论是这是仅库的元组实现的一个缺点.它们并不像我们希望的那样神奇.

编辑:

事实证明,似乎并没有解决所有上述问题的真正解决方案.一个非常好的答案可以解释为什么这是不可解决的.特别是,我希望有人能够阐明为什么"失败的尝试"无法发挥作用.


(1):一个可怕的黑客是将转换写为模板并转换为转换运算符中请求的成员类型.这是一个可怕的黑客,因为我不知道在哪里存储这些转换成员.在这个演示中,我使用静态变量,但这不是线程可重入的.

(2):可以应用与(1)中相同的黑客.

dyp*_*dyp 18

Why the current attempts fail

std::tie(a, b) produces a std::tuple<int&, string&>. This type is not related to std::tuple<int, string> etc.

std::tuple<T...>s have several assignment-operators:

  • A default assignment-operator, that takes a std::tuple<T...>
  • A tuple-converting assignment-operator template with a type parameter pack U..., that takes a std::tuple<U...>
  • A pair-converting assignment-operator template with two type parameters U1, U2, that takes a std::pair<U1, U2>

For those three versions exist copy- and move-variants; add either a const& or a && to the types they take.

The assignment-operator templates have to deduce their template arguments from the function argument type (i.e. of the type of the RHS of the assignment-expression).

Without a conversion operator in Foo, none of those assignment-operators are viable for std::tie(a,b) = foo. If you add a conversion operator to Foo, then only the default assignment-operator becomes viable: Template type deduction does not take user-defined conversions into account. That is, you cannot deduce template arguments for the assignment-operator templates from the type Foo.

Since only one user-defined conversion is allowed in an implicit conversion sequence, the type the conversion operator converts to must match the type of the default assignment operator exactly. That is, it must use the exact same tuple element types as the result of std::tie.

To support conversions of the element types (e.g. assignment of Foo::a to a long), the conversion operator of Foo has to be a template:

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

However, the element types of std::tie are references. Since you should not return a reference to a temporary, the options for conversions inside the operator template are quite limited (heap, type punning, static, thread local, etc).


Ded*_*tor 6

There are only two ways you can try to go:

  1. Use the templated assignment-operators:
    You need to publicly derive from a type the templated assignment-operator matches exactly.
  2. Use the non-templated assignment-operators:
    Offer a non-explicit conversion to the type the non-templated copy-operator expects, so it will be used.
  3. There is no third option.

In both cases, your type must contain the elements you want to assign, no way around it.

#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';
}
Run Code Online (Sandbox Code Playgroud)

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

  • @MSalters从元组派生应允许转换(使用`std :: tuple`的赋值运算符模板)和const成员. (2认同)

lee*_*mes 5

正如其他答案已经解释的那样,您必须继承 a tuple(为了匹配赋值运算符模板)或转换为完全相同tuple的引用(为了匹配采用相同引用的非模板化赋值运算符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}
    {}
};
Run Code Online (Sandbox Code Playgroud)

这种“附加关系”使得可以将(非常量!)分配Foo给可转换组件类型的关系。由于“附加关系”是一个引用元组,因此它会自动分配成员的当前值,即使您在构造函数中对其进行了初始化。

为什么是“附领带”呢const?因为否则, aconst Foo可以通过其附加的领带进行修改。

领带的非精确组件类型的用法示例(注意 vs long longint

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

    long long a;
    string b;

    tie(a, b) = foo;
    cout << a << ' ' << b << '\n';
}
Run Code Online (Sandbox Code Playgroud)

将打印

42 bar
Run Code Online (Sandbox Code Playgroud)

现场演示

因此,这通过引入一些空间开销解决了问题 1. + 3.。