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

Make custom type "tie-able" (compatible with std::tie)

本文关键字:tie 兼容 std 自定义 类型 tie-able      更新时间:2023-10-16

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

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 + 推断 operator= 的模板参数 X"不能一起工作,一次只能使用其中一个。

不完美的尝试:

因此,我尝试为领带的确切类型实现一个转换运算符:

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

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

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

只看到提供我自己的tie函数+类以及我的"可绑定"对象的替代方案,这使我被迫复制我不喜欢的std::tie的功能(不是我觉得这样做很难,但必须这样做感觉不对(。

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

编辑:

事实证明,似乎没有解决上述所有问题的真正解决方案。一个很好的答案可以解释为什么这是无法解决的。特别是,我希望有人阐明为什么"失败的尝试"不可能奏效。


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

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

为什么当前尝试失败

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. 使用模板化赋值运算符:
    您需要从模板化赋值运算符完全匹配的类型公开派生。
  2. 使用非模板化赋值运算符:
    提供非模板化复制运算符期望的非explicit转换,以便使用它。
  3. 没有第三种选择。

在这两种情况下,您的类型都必须包含要分配的元素,这是无法解决的。

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

关于大肠杆菌:http://coliru.stacked-crooked.com/a/315d4a43c62eec8d

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

如果你从元组继承,你将失去命名的成员,即 foo.a不再可能。

在这个答案中,我提出了另一种选择:如果你愿意支付一些空间开销(每个成员的常量(,你可以通过继承常量引用的元组来同时拥有命名成员元组继承,即对象本身的常领带:

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

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

为什么"附加领带"const?因为否则,可以通过其连接的领带修改const Foo

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

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.

这种可以做你想要的对吗?(假设您的值可以链接到课程的类型...

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

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

其次,转换运算符不是显式的,它将转换为请求的任何元组类型(可能您不希望这样做......

主要优点是您不必明确指定转换类型,这一切都是推断出来的......

这段代码对我有用。如果有人能指出它的任何错误,我会很高兴。

编译器资源管理器上的简单版本

编译器资源管理器上的更多通用版本

#include <tuple>
#include <cassert>
struct LevelBounds final
{
  int min;
  int max;
  operator std::tuple<int&, int&>() { return {min, max}; }
};
int main() {
    
    int a, b;
    auto lb = LevelBounds{30, 40};
    std::tie(a, b) = lb;
    assert(30 == a);
    assert(40 == b);    
    return 0;
}