How can I specify the return type of a function that returns the result of a ranges pipeline? [duplicate]
Asked Answered
S

3

7

Consider the following scenario:

struct MyInterface {};

struct Data : MyInterface{};

struct DataWrapper
{
    SomeContainerOf<MyInterface*> getData()
    {...}
private:
    std::vector<Data> dataStorage;
};

In terms of my requirements, I need the accessor function for DataWrapper to return a container of MyInterface* values (pointers because we are really returning Data values).

Ideally, I want to do this without making a copy. From what I can tell, something like the following is the idiomatic C++ way to do this:

auto DataWrapper::getData()
{
    return dataStorage
        | std::ranges::views::transform(
            [](Data& d) -> MyInterface* { return &d; }
        );
}

However, making the return type auto obscures the return type, and I want the to be explicit about what it returns. It seems like (I could be wrong here) you aren't supposed to know what the type of a view is (in a similar style to lambdas).

The alternative to this that I'm considering would be to make a custom view (where I could make the type a little more clear), but it really pains me to reimplement something that is so clearly in the standard. What would be the recommended move for such a scenario? Does modern C++ say that I should keep the return value auto?

Streptothricin answered 1/6 at 20:58 Comment(7)
As I understand, no, you are not supposed to know the exact type and yes, you are supposed to use auto.Deemster
Surely you have documentation to which your users refer rather than relying entirely on reading the declarations?Astor
@DavisHerring absolutely, but I feel an interface should still be legible without the documentation. Documentation also frequently includes a functions return type, so to be able to provide a concept to describe this (as top answer has taught me) seems like the best of all worlds.Streptothricin
@Thornsider3: In the absence of ABI requirements, it’s often more helpful to document that a function returns “a range of an unspecified integral type” or so than to lean on the language return type (perhaps included automatically).Astor
@DavisHerring why? My deeper concern with auto is that a user mistakes what type gets returned and the code still compiles (e.x. I pass some auto return function to std::cout and I print the class rather than the member).Streptothricin
@Thornsider3: Mostly because a client who sticks to an abstract interface like "this returns a range of (something generic)" is less likely to be broken by internal reorganization or bug fixes. I don't think that using a deduced return type makes it materially more likely that users will be wrong about that return type (are they literally guessing what it might be?).Astor
Dupe: a more explicit type instead of auto for std::ranges::views resultJeter
N
14

Does modern C++ say that I should keep the return value auto?

If you are concerned that auto is too implicit in expressing the return type, using constrained auto may be an appropriate choice. For example

template <typename R, typename V>
concept RangeOf = std::ranges::range<R> && 
                  std::same_as<std::ranges::range_value_t<R>, V>;

struct DataWrapper
{
    RangeOf<MyInterface*> auto getData()
    {
      return dataStorage
        | std::ranges::views::transform(
            [](Data& d) -> MyInterface* { return &d; }
        );
    }
private:
    std::vector<Data> dataStorage;
};
Newsstand answered 2/6 at 2:49 Comment(2)
This is a great suggestion, and in a sense more straightforward and informative than my recommendation #1!Burschenschaft
You could be even more specific: concept RandomAccessRangeOf = std::ranges::random_access_range<R> && std::same_as<std::ranges::range_value_t<R>, V>Lim
B
5

I'll make several semi-orthogonal recommendations:

Recommendation 1: Clarify the return type with a type definition

I need it to be known that this method returns this list

So, do that! Instead of declaring the function as returning auto:

  1. Factor out the lambda - either into a static private member function, or a function in a detail_ namespace etc. Now you can use it, or its type, without repeating it. Suppose you've defined it as called foo()
  2. Write something like:
    using bar = std::decltype(dataStorage | std::ranges::views::transform(foo));
    
    and of course you will replace bar with the type you want the users of this method to know the result by.

Note: 康桓瑋's answer suggests using a constrained auto, for a similar effect. That's a good idea, possibly even better than this one (although slightly more verbose). So please upvote their answer.

Recommendation 2: Carefully consider the need for inheritance

Before you even consider the "bigger" DataWrapper class, holding the vector - perhaps you should think about whether you really need to use MyInterface rather than Data. You seem to be relying to some extent on actually returning Data's from getData() - hence the name.

  • Do you have other inheritors of MyInterface? If not, consider dropping it (or keeping it, but not using it other than to streamline the way you define Data).
  • Are there many inheritor classes of MyInterface? Or are they not known at the point of definition of MyInterface? If the answer is "no", consider using an std::variant<Data,C1,C2,C3> for the case of C1, C2, C3 being the inheritors of MyInterface.
  • Can you templatize the code accessing MyInterface child classes? You might make MyInterface into a concept rather than a base class. Or CRTP might come into play somehow.

If you can do any of those, it's possible you could avoid the need for a DataWrapper class _completely, and simply have an std::vector<foo> for some relevant foo (Data, or the variant, class, or even std::any).

I'm not saying that avoiding inheritance is "better"; it means switching from one tradeoff of benefits and detriments to another. But many people just opt for inheritance because it's the more traditional way of doing things in C++ rather than being what best suits their needs.

Recommendation 3: Don't return a container (or range), be the container!

You have a value-type class (DataWrapper) with a vector of data. Now, you're not willing to let people use that vector directly, so DataWrapper can't be used as a C++ container of Datas.

Well - exploit that! Make it usable as a container of objects-inheriting-MyInheritance's, i.e. MyInheritance references! Implement begin, cbegin, size, empty, iterator and all the other parts of that named requirement. Perhaps go as far as implementing everything in ContiguousContainer... while that will make your class somewhat more complex, you will actually be saving the indirect complexity of using std::ranges (whole lot of code under the hood there), plus - your users won't have to know about getData(), so use-wise, you're probably simplifying things.

Burschenschaft answered 1/6 at 21:44 Comment(0)
V
2

Since the capture list of lambda is empty, default constructor can instantiate it. So, we can verbosely state the return type. But since range adapter constructors are explicit, some code repetition takes place:

namespace rng = std::ranges;
auto DataWrapper::getData() ->
rng::transform_view<
     rng::ref_view<decltype(dataStorage)>,
     decltype ([](Data& d) -> MyInterface& { return d; })
> {//DataWrapper::getData()
     return rng::transform_view{
            rng::ref_view{dataStorage},
            {/*lambda::lambda()*/}
     };//! return 
};//!DataWrapper::getData()

I used ref_view in the snippet, because I couldn't verify if the first type argument to transform_view should be a container, or a reference to a container. But ref_view satisfies the constraints and embeds a reference to the container.

Vanzant answered 2/6 at 16:57 Comment(2)
Thanks - two questions: 1: Why do you state an auto return type and then state it later as a trailing return type? I've seen this pattern before when the return type is dependent on the input arguments, but I don't understand it here. 2: Is this preferred over an auto return type? My issue with this is exactly what you said : we've repeated ourself (both with the lambda definition and with the return type).Streptothricin
Trailing return type is a C++14 feature that is normally used on member functions with member return types, to enable shortening(dropping) prefixed namespace/class qualifiers. But it's also the only available option for lambdas. I prefer this syntax for long return types over the old school, because of increased readability. There are other cases where this syntax is more readable. It also enables to modify the code to/from lambda with minimum effort(add/remove =[] between the name and argument list).Vanzant

© 2022 - 2024 — McMap. All rights reserved.