【问题标题】:Make custom type "tie-able" (compatible with std::tie)使自定义类型“可绑定”(与 std::tie 兼容)
【发布时间】:2014-10-09 11:13:24
【问题描述】:

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

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   Demo
  • operator tuple<int&,string&>()   Demo

分配现在有效,因为类型现在(转换后)完全相同,但这不适用于我想支持的三个场景:

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

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

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

编辑:

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


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

(2): 可以应用与 (1) 相同的技巧。

【问题讨论】:

  • 我对此感到非常震惊,但这个优秀问题似乎没有预先存在的副本。
  • @MagnusHoff: operator= 必须是成员 ([C++11: 13.5.3/1]),您不能更改 tuple<int, string> 的定义。
  • 我认为基本问题如下:std::tie 产生一个引用元组,即tuple<int&, string&>。无论您将 Foo 转换为什么,它都不能包含 const 转换运算符的非常量引用。因此转换为的类型与tie 类型不匹配。因此,默认的赋值运算符是不可行的,必须使用赋值运算符模板。但是这个模板的推论失败了,因为Foo本身不是tuple
  • “失败的尝试”不起作用,因为模板参数推导(发生在重载决议之前)不考虑用户定义的隐式转换(在重载决议期间考虑)。
  • @leemes 有两个非模板operator=s 接受完全相同的类型。

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


【解决方案1】:

为什么当前尝试失败

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

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

  • 一个默认的赋值操作符,它接受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 的元素类型是引用。 由于您不应该返回对临时的引用, 运算符模板内的转换选项非常有限 (堆、类型双关语、静态、线程本地等)。

【讨论】:

  • 附带说明:我猜想他们与std::pair 有同样的问题,因为他们想让它与双组件std::tuple 完全兼容(即无缝)。因此他们需要添加这个重载。 [与他们 := 标准委员会]
  • @leemes 是的,这就是构造函数和赋值运算符的专用重载内置于std::tuple 的原因。在我看来,将这两个类捆绑在一起就像是一种向后兼容性黑客。
  • @0x499602D2 因为我将问题读作“如何使类型可绑定?”这并不能回答问题。它说我们不能在满足某些要求(或优雅地)的同时做到这一点,但是std::tie 有替代品。
  • @dyp 我记得我曾经在一个元线程中读到,每当一个问题“没有答案”(即不可能按要求做)时,一个可行的答案就可以解释为什么这是案例(并可能提出替代方案)。
【解决方案2】:

只有两种方法可以尝试:

  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

【讨论】:

  • 您的两个解决方案都失败了 op 需求 1,支持转换,需求 2,支持 const 成员,const struct Y 无法转换失败的需求 3..
  • @MSalters 从元组派生应该允许转换(使用 std::tuple 的赋值运算符模板)和 const 成员。
  • @leemes:我概述了您的自定义类型可以分配给std::tie(...) 的所有方式,这也解释了为什么您的失败尝试不起作用:它们都不属于两个可分配的类别。
  • @LightnessRacesinOrbit:g++ 给出的一个警告是误报,而 clang++ 没有给出任何警告。我对XY 使用带有空列表的统一初始化,这意味着X 的默认ctor(值初始化所有元组成员)和聚合初始化(值初始化所有Y -成员)为Y。错误在哪里?
  • @Deduplicator:嗯,嗯,嗯,很好:P 不过,为了让这个例子有说服力,我还是选择了一个独特的值。输出零并不能让我对任何特定的演示充满信心。
【解决方案3】:

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

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

在这个答案中,我提出了另一种选择:如果您愿意支付一些空间开销(每个成员不变),您可以同时拥有 named memberstuple通过从 const 引用元组继承来同时继承,即对象本身的 const tie:

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 可以通过其附加的 tie 进行修改。

领带的非精确组件类型的使用示例(注意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

Live demo

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

【讨论】:

  • 从引用元组继承会打开 const 正确性的漏洞:coliru.stacked-crooked.com/a/3db6565a6b3f73f4
  • @dyp 我只是想知道这会导致什么结果。刚刚在您写的那一刻看到了问题... ;) 但是,从 const refs 的元组继承是可行的。
  • 是的,但它会阻止foo = tie(a,b)
  • @dyp 是的,但这不完全是问题的一部分(尽管它会很好)。我编辑了一个 const refs 元组的答案,感谢您的提示。
【解决方案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;

}

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

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

最大的好处是不用显式指定转换类型,都是推导出来的……

【讨论】:

  • 为什么不用get&lt;Type&gt; 而不是get&lt;Index&gt;?这可能需要另一个间接方法来挂钩索引序列,以便将推导的类型与索引一起解包。
  • @leemes,好吧,如果你有多个相同类型的成员,你将需要一个索引(以及获取类型的方法),但主要的缺点是你并不真的知道哪个成员应该在哪个索引处(不暴露实现细节,例如成员的顺序......)我想在这种情况下,你最好的选择是在构建领带时围绕引用进行“命名包装”,并且在形成元组时将其提取为“类型”..)
  • 顺序是暴露的,因为导出的元组是有序的......所以我会像get-order 一样,就像元组接口的顺序一样。它们当然可以独立于结构成员的实际顺序。
  • @leems,不,你可以更改顺序,例如,在上面的代码中,将tie(a, b) = foo;更改为tie(b, a) = foo;,这样也可以正常工作...
  • 啊,这就是你的意思,这也是你对“类型指定”吸气剂的想法。如果成员具有相同的类型,我不太喜欢它,因为明确性...
猜你喜欢
  • 2023-02-17
  • 1970-01-01
  • 1970-01-01
  • 2020-07-31
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多