1

I'm trying to use views in a commercial application, and noticed an inconsistency between gcc and Visual Studio.

In the code below, calling transformed() twice returns two different, apparently incompatible views. In gcc 11 (on godbolt), the code executes without issue, even with extra debugging, but in Visual Studio 16.11 with -std:c++latest, it asserts:

cannot compare incompatible transform_view iterators

I would like my function to be callable just as if it were returning a const std::vector<std::pair<int, int>> & so the caller doesn't have to worry about temporaries. It seems that I could make my transformed view a member of my class, initialize it in the constructor, and return that, but I don't even know how to declare it.

I'm assuming that Visual Studio is correct and my code is illegal, but even if my code should be legal, it still has to work. We have a 10,000,000-line code base and a lot of non-expert C++ programmers, and I need the core to be robust and not have hidden gotchas like this.

#include <iostream>
#include <ranges>
#include <vector>

struct X
{
    std::vector<int> m_values{ 1,2,3 };
    auto transformed() const
    {
        return std::ranges::views::transform(m_values, [](int i) {
            return std::pair{ i, i + i };
            });
    }
};

int main()
{
    X x;
    for (auto [a, b] : x.transformed())
        std::cout << a << " " << b << std::endl;

    if (x.transformed().begin() != x.transformed().end()) // asserts in visual studio.
        std::cout << "not empty";

    return 0;
}

https://godbolt.org/z/hPWYGn9dY

Remy Lebeau
  • 555,201
  • 31
  • 458
  • 770
Rob L
  • 2,351
  • 13
  • 23
  • 1
    you can't compare iterators from two different views. Imagine you have two vectors: `std::vector a; std::vector b;` your if statement would essentially do `if(a.begin() != b.end())` - which is undefined behaviour because you're comparing iterators from two different containers. it's the same concept with views - whenever you're dealing with iterators, both must come from the same container. – Turtlefight Nov 19 '21 at 18:38
  • you could fix this by changing your if statement to e.g. `if (auto view = x.transformed(); view.begin() != view.end())` that way you're comparing iterators from the same view. – Turtlefight Nov 19 '21 at 18:40

1 Answers1

2

It seems that I could make my transformed view a member of my class, initialize it in the constructor, and return that, but I don't even know how to declare it.

You can turn X into a template class, and construct the member transform_view through the passed lambda, something like this:

#include <iostream>
#include <ranges>
#include <vector>

template<class F>
struct X {
  std::vector<int> m_values{1,2,3};
  decltype(m_values | std::views::transform(std::declval<F>())) m_transformed;

  X(F fun) : m_transformed(m_values | std::views::transform(std::move(fun))) { }
  const auto& transformed() const { return m_transformed; }
};

int main() {
  X x([](int i) { return std::pair{i, i + i}; });
  for (auto [a, b] : x.transformed())
    std::cout << a << " " << b << std::endl;

  if (x.transformed().begin() != x.transformed().end())
    std::cout << "not empty";
}

Demo.

Another way is to use std::function, which makes your X need not be a template class:

struct X {
  using Fun = std::function<std::pair<int, int>(int)>;
  std::vector<int> m_values{1,2,3};
  decltype(m_values | std::views::transform(Fun{})) m_transformed;

  X(Fun fun = [](int i) { return std::pair{i, i + i}; }) 
  : m_transformed(m_values | std::views::transform(std::move(fun))) { }
  const auto& transformed() const { return m_transformed; }
};

Demo.

康桓瑋
  • 33,481
  • 5
  • 40
  • 90
  • Thanks, perfect (although a little more verbose than I'd like given the simplicity of the original, but that's C++ for you). I also did the non-template version starting with your code at https://godbolt.org/z/4TxWsrfYY. – Rob L Nov 19 '21 at 19:12
  • 2
    @RobL keep in mind though that `for (auto [a, b] : X().transformed())` will also not result in a compile time error, but will blow up at runtime (X will be freed before the for loop runs) [godbolt](https://godbolt.org/z/4654c11s3) – Turtlefight Nov 19 '21 at 19:24
  • @康桓瑋 I updated the code derived from your solution https://godbolt.org/z/jaqz1Wv71. This has an additional advantage for me that I have a function returning the transformation which can be called as a free function in other places. Thanks again. – Rob L Nov 19 '21 at 19:34
  • @Turtlefight agreed. For me, the object X is our main database and is unlikely to be used in that way. But, that's a general C++ gotcha, std::string().c_str() passed has the same issue. If X were frequently were created as a temporary, then this solution of holding the transformed view could possibly make things worse (maybe). – Rob L Nov 19 '21 at 19:43