自定义types“可绑定”(与std :: tie兼容)

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

struct Foo { int a; string b; }; 

我如何使这个对象的实例可分配给一个std::tie ,即std::tuple引用的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 ”。 我想这是因为“转换为任何types的X +演绎模板参数X for operator =”不能一起工作,只能同时使用其中的一个。

不完美的尝试:

因此,我试图为领带确切types实现一个转换操作符:

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

现在的作业是可行的,因为types现在(转换后)完全一样,但是这不适用于我想支持的三种场景:

  1. 如果绑定具有不同但可转换types的variables绑定(即在客户端更改int a; long long a; ),则失败,因为types必须完全匹配。 这与将元组分配给允许可转换types的引用元组的通常使用相矛盾。 (1)
  2. 转换运算符需要返回必须给出左值引用的联系。 这不适用于临时值或const成员。 (2)
  3. 如果转换运算符不是const,那么对于右边的const Foo ,赋值也会失败。 要实现一个const转换的版本,我们需要破解const主体成员的const。 这是丑陋的,可能会被滥用,导致未定义的行为。

我只能看到另外一个select,就是提供我自己的tiefunction+课程以及“tie-able”对象,这使我强制复制我不喜欢的std::tiefunction(不是说我觉得很难这样做,但必须这样做感觉不对)。

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

编辑:

事实certificate,似乎没有解决所有上述问题的真正解决scheme。 一个很好的答案将解释为什么这是不可解的。 特别是,我希望有人澄清为什么“失败的尝试”不可能奏效。


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

(2):可以使用与(1)中相同的方法。

为什么当前的尝试失败

std::tie(a, b)产生一个std::tuple<int&, string&> 。 这个types与std::tuple<int, string>等无关

std::tuple<T...>有几个赋值运算符:

  • 一个默认的赋值运算符,它接受一个std::tuple<T...>
  • 带有types参数包U...的元组转换赋值运算符模板 ,它接受一个std::tuple<U...>
  • 具有两个types参数U1, U2的对转换赋值运算符模板 ,其采用std::pair<U1, U2>

对于这三个版本存在复制和移动变体; 添加一个const&或一个&&到他们所采取的types。

赋值运算符模板必须从函数参数types(即赋值expression式的RHStypes)中推导出它们的模板参数。

如果没有Foo的转换运算符,则这些赋值运算符都不可用于std::tie(a,b) = foo 。 如果将转换运算符添加到Foo ,则只有默认的赋值运算符才可行:模板types扣除不考虑用户定义的转换。 也就是说,你不能从Footypes推导赋值运算符模板的模板参数。

由于在隐式转换序列中只允许一个用户定义的转换,转换运算符转换的types必须完全匹配默认赋值运算符的types。 也就是说,它必须使用与std::tie结果完全相同的元组元素types。

为了支持元素types的转换(例如将Foo::a赋值为long ), Foo的转换运算符必须是一个模板:

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

但是, std::tie的元素types是引用。 既然你不应该返回一个临时的引用,运算符模板中的转换选项是相当有限的(堆,types双精度,静态,线程本地等)。

只有两种方法可以尝试去:

  1. 使用模板化的赋值运算符:
    您需要从模板化的赋值运算符完全匹配的types公开派生。
  2. 使用非模板赋值运算符:
    提供非模板化的复制操作符所期望的types的非explicit转换,以便它将被使用。
  3. 没有第三种select。

在这两种情况下,你的types都必须包含你想要分配的元素,没有办法绕过它。

 #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

正如其他答案已经解释过的,你必须从一个tupleinheritance(为了匹配赋值运算符模板)或者转换成完全相同的引用tuple (为了匹配非模板赋值运算符,相同的types)。

如果你从一个元组inheritance,你将失去指定的成员,即foo.a不再是可能的。

在这个答案中,我提出了另一个select:如果你愿意付出一定的空间开销(每个成员的常量),你可以同时拥有两个命名成员以及一个元组inheritance,从一个const引用元组inheritance,即一个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分配给一系列可转换组件types。 由于“附加领带”是一个引用的元组,它会自动赋值成员的当前值,尽pipe你在构造函数中初始化它。

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

带有非精确元素types的示例用法(注意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。

这种做你想做的事吧? (假设你的价值可以链接到课程的types…)

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

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

其次转换运算符是不明确的,它将转换为任何请求的元组types(可能是你不希望那..)

主要优点是你不必明确指定转换types,这一切都推断…