是否可以交换两个 std::tie 调用的结果

Is it possible to swap the result of two std::tie calls

提问人:Tom 提问时间:9/15/2023 最后编辑:Tom 更新时间:9/27/2023 访问量:113

问:

一段时间以来,我一直在尝试找到一种符合标准的方法来编译这样的东西:

int a = 1;
int b = 2;
int c = 3;
int d = 4;

std::swap(std::tie(a, b), std::tie(c, d));

(用例更复杂(与zip迭代器有关),但这是问题的核心)。

编译失败的原因是需要左值引用,但是因为返回 ,你可以让它交换它间接引用的元素(即使是右值)。std::swapstd::tiestd::tuple<T&...>std::tuple

实现此目的的一种方法是向命名空间添加重载:std::swapstd

namespace std {
  template<typename... Refs>
  void swap(std::tuple<Refs&...> lhs, std::tuple<Refs&...> rhs) {
    lhs.swap(rhs);
  }
}

但是,我知道这不符合标准......

另一种选择是添加一个本质上是包装器的类型,并在您自己的命名空间中添加一个重载。 然后,当从另一个标准算法(如 )使用该类型时,可以通过参数相关查找 (ADL) 找到。std::tupleswapswapstd::sort

template<typename... Ts>
struct tuple_wrapper {
  std::tuple<Ts...> ts_;
  tuple_wrapper(Ts... ts) : ts_(ts...) {}
  // implementation...
};

这样做的问题是,至少😅可以说,尝试编写一个透明的包装器被证明是棘手的(如果我尝试使用该类型,我经常会遇到编译错误页面,并且编写一个新的复杂类型来模仿似乎并不理想......std::tuplestd::sortstd::tuple

有没有人对如何使用符合标准的解决方案以简单的方式解决这个问题有任何建议?任何想法将不胜感激。

谢谢!

更新

理想情况下,我想让它在 C++ 17 中工作,感谢康桓瑋指出由于 P2321,这实际上在 C++ 23 中有效。

更新 2

这是此示例所基于的类的片段...

template<typename... InputIts>
class zip_iterator {
  std::tuple<InputIts...> its_;

public:
    using iterator_category = std::random_access_iterator_tag;
    using value_type =
      std::tuple<typename std::iterator_traits<InputIts>::value_type...>;
    using difference_type = std::common_type_t<
      typename std::iterator_traits<InputIts>::difference_type...>;
    using pointer =
      std::tuple<typename std::iterator_traits<InputIts>::pointer...>;
    using reference =
      std::tuple<typename std::iterator_traits<InputIts>::reference...>;

    zip_iterator(InputIts... its) : its_(its...) {}

    reference operator*() {
      return std::apply([](auto&... its) { return std::tie(*its...); }, its_);
    }

    // ... rest of implementation, mostly other operators
};

我一直遇到的问题是 ,以及它如何与 然后 交互。referencestd::swapstd::sort

请注意,此实现的灵感来自这篇文章以供参考 - https://codereview.stackexchange.com/questions/231352/c17-zip-iterator-compatible-with-stdsort。我只是想看看如果可能😅的话,我是否可以创建一个稍微简单的实现


更新:事实证明,我偶然发现了一个比我最初预期的“代理迭代器”要复杂得多的话题。要了解这里的全部复杂性,请阅读埃里克·尼布勒(Eric Niebler)的以下四篇文章:

长话短说,正确地做到这一点(所以只移动类型被正确处理等等)需要对标准库进行更改。

似乎最明智的建议是等待 C++23 或像我应该做🙈的那样使用 ranges-v3

C++ 模板 std stdtuple

评论

1赞 康桓瑋 9/15/2023
由于 P2321,您的代码在 C++23 中符合良好要求
2赞 André 9/15/2023
尽管您说用例感觉更复杂,但它仍然感觉像是一个 XY 问题。如果您正在交换某行代码...为什么不按正确的顺序和特定的代码行进行分配呢?std::tie( c, d )std::tie( a, b )
0赞 Alan Birtles 9/15/2023
我不只是写你自己的交换函数吗?godbolt.org/z/eve618G48
0赞 Tom 9/15/2023
感谢您到目前为止的回复!我应该提到我正在尝试让它在 C++ 17 中编译,有趣的是知道这在 C++23 中已修复。
0赞 Tom 9/15/2023
不幸的是,我无法使用自定义名称编写自己的交换,因为我需要在其他上下文中通过 ADL 找到它(例如 std::sort)

答:

1赞 Tom 9/18/2023 #1

好的,我有一个解决方案,我至少对它相当满意😅。

非常感谢康桓瑋,André,Alan Birtles,joergbrech和Jarod42的评论,他们帮助我朝着正确的方向前进(特别是康桓瑋,他引用了P2321和C++ 23解决方案)。

如果您使用的是 C++ 23,请立即停止阅读。此问题已修复,并且仅适用于 TM。

如果您仍然坚持使用 C++ 20 或 C++ 17,请继续阅读...

要变通解决此问题(并避免需要向命名空间添加重载),可以在 周围添加包装类型。stdstd::tuple

以下是几个几乎可以肯定不完整的选项(并且在某些用法下可能会失败),但目前它们对我有用。我将很快为此编写一堆测试和基准测试,并将在此处链接到这些测试和基准测试以供将来参考,希望能有更多的修复/改进。

有两种可能的选择,我还没有决定哪个更可取(需要更多的调查/测试/分析),但我现在会把它们都包括在这里。

选项 1

创建一个类型并包含一个隐式构造函数,以便从 进行转换。tuple_wrappertuple_wrapperstd::tuple

namespace thh {
template <typename... Ts>
struct tuple_wrapper {
    std::tuple<Ts...> ts_;
    tuple_wrapper(Ts... ts) : ts_(ts...) {}
    // implicit conversion from std::tuple
    tuple_wrapper(std::tuple<Ts...> tup) : ts_(std::move(tup)) {}
};

// swap overload for tuple_wrapper of references
template <typename... Refs>
void swap(tuple_wrapper<Refs&...> lhs, tuple_wrapper<Refs&...> rhs) {
    lhs.ts_.swap(rhs.ts_);
}
} // namespace thh

// usage
int a = 1;
int b = 2;
int c = 3;
int d = 4;
using std::swap;
swap(thh::tuple_wrapper(std::tie(a, b)),
     thh::tuple_wrapper(std::tie(c, d)));

Compiler Explorer 演示

选项 2

创建一个类型并提供我们自己的实现(请参阅此处的参考实现。tuple_wrapperstd::tie

namespace thh {
template <typename... Ts>
struct tuple_wrapper {
    std::tuple<Ts...> ts_;
    tuple_wrapper(Ts... ts) : ts_(ts...) {}
};

// our own version of tie that returns a tuple_wrapper
template <typename... Args>
constexpr tuple_wrapper<Args&...> tie(Args&... args) noexcept {
    return {args...};
}

// swap overload for tuple_wrapper of references
template <typename... Refs>
void swap(tuple_wrapper<Refs&...> lhs, tuple_wrapper<Refs&...> rhs) {
    lhs.ts_.swap(rhs.ts_);
}
}  // namespace thh

// usage
int a = 1;
int b = 2;
int c = 3;
int d = 4;
using std::swap;
swap(thh::tie(a, b), thh::tie(c, d));

Compiler Explorer 演示

第一个也许更好?我还不确定是否有任何理由更喜欢一个而不是另一个(约定/兼容性/性能?我将进行更多的测试,如果我找到更好的答案,我会报告。

以上内容有望回答我最初的问题。我所指的稍微复杂一点的用例变得有点复杂(需要更多的操作员才能很好地使用)。std::sort

我目前的(可能是可悲的不完整的实现)看起来像这样:

namespace thh {
  template<typename... Ts>
  struct tuple_wrapper {
    std::tuple<Ts...> ts_;
    tuple_wrapper(Ts... ts) : ts_(ts...) {}
    tuple_wrapper(std::tuple<Ts...> tup) : ts_(std::move(tup)) {}
    template<typename... UTypes>
    tuple_wrapper& operator=(tuple_wrapper<UTypes...>&& other) {
      ts_ = std::move(other).ts_;
      return *this;
    }
    template<typename... UTypes>
    tuple_wrapper& operator=(const tuple_wrapper<UTypes...>& other) {
      ts_ = other.ts_;
      return *this;
    }
    template<typename... UTypes>
    tuple_wrapper(tuple_wrapper<UTypes...>&& other) {
      ts_ = std::move(other).ts_;
    }
    template<typename... UTypes>
    tuple_wrapper(const tuple_wrapper<UTypes...>& other) {
      ts_ = other.ts_;
    }
    template<size_t I>
    auto& get() {
      return std::get<I>(ts_);
    }
    template<size_t I>
    const auto& get() const {
      return std::get<I>(ts_);
    }
    bool operator==(const tuple_wrapper& rhs) const { return ts_ == rhs.ts_; }
    bool operator!=(const tuple_wrapper& rhs) const { return !(*this == rhs); }
  };
} // namespace thh

我不打算在这里粘贴所有代码,但您可以在下面找到迭代器的完整实现:zip

Compiler Explorer 演示

这个实现很大程度上受到这个问题和 DarioPZip 实现的启发。有关替代实现,请参阅 ZipIterator GitHub 项目。

此外,对于一些额外的上下文,促使这样做的是研究更有效的方法,在不需要分配额外内存的情况下对多个范围进行排序。我之前使用的方法在这篇博文中进行了描述 - 间接排序

再次感谢,我希望这对其他人也有用 - 一旦全部整理和测试,我将用这段代码向我的 repo 添加一个链接!😅