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

时间:2014-10-09 11:13:24

标签: c++ c++11 tuples std

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

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的赋值运算符是不可能的,因为赋值运算符是必须是左侧对象成员的二元运算符之一。

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

  • 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. 如果绑定具有绑定的不同但可转换类型的变量(即在客户端更改int a;long long a;),则由于类型必须完全匹配而失败。这与将元组赋值给允许可转换类型的引用元组的通常用法相矛盾。(1)
  2. 转换运算符需要返回一个必须赋予左值引用的平局。这不适用于临时值或const成员。(2)
  3. 如果转换运算符不是const,则右侧的const Foo的赋值也会失败。要实现转换的const版本,我们需要破坏const主题成员的常量。这很丑陋,可能会被滥用,导致行为不确定。
  4. 我只看到提供我自己的tie函数+类以及我的“可绑定”对象的替代方法,这使我强制复制std::tie我不喜欢的功能(并不是说我觉得很难这样做,但是必须这样做是错误的。)

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

    修改

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


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

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

4 个答案:

答案 0 :(得分:18)

当前尝试失败的原因

std::tie(a, b)生成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的元素类型是引用。 既然你不应该返回一个临时的引用, 运算符模板内的转换选项非常有限 (堆,类型惩罚,静态,线程本地等)。

答案 1 :(得分:6)

您只能尝试两种方式:

  1. 使用模板化赋值运算符:
    您需要从模板中公开派生模板化赋值运算符。
  2. 使用非模板化赋值运算符:
    提供非explicit转换为非模板化复制操作符所期望的类型,因此将使用它。
  3. 没有第三种选择。
  4. 在这两种情况下,您的类型必须包含您要分配的元素,无法绕过它。

    #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

答案 2 :(得分:4)

正如其他答案已经解释的那样,您必须继承tuple(以匹配赋值运算符模板)或转换为完全相同的tuple引用(以便匹配)非模板化赋值运算符采用相同类型的tuple个引用。)

如果你从一个元组继承,你就会失去指定的成员,即foo.a不再可能。

在这个答案中,我提出了另一个选择:如果你愿意支付空间开销(每个成员不变),你可以同时拥有命名成员元组< / em>继承来自const引用的元组,即对象本身的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?因为否则,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

Live demo

所以这解决了问题1. + 3.引入空间开销。

答案 3 :(得分:2)

这种做你想要的吗? (假设您的值可以与课程类型相关联......)

#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;

}

主要的缺点是每个成员只能通过其类型访问,现在,您可以使用其他一些机制来解决这个问题(例如,为每个成员定义一个类型,并通过成员类型包装对该类型的引用你想访问..)

其次转换运算符不明确,它会转换为请求的任何元组类型(可能你不想要那个......)

主要优点是您不必明确指定转换类型,所有内容都已推断......