Recently, I discovered that std::ranges prohibits the creation of dangling iterators and provides an owning_view to take ownership of temporaries. Digging into the details led me to the ref-qualified memeber functions which can be used to make code safer and/or more performant.

Article updates (2023-05-19)

Intro (the source of my excitement)

Consider the following code that creates a temporary array using get_array_by_value, then uses the temporary when safe, rejects compilation when not safe, and takes ownership of the temporary when needed.

Although std::ranges is not the primary focus of this article, it’s worth to briefly touch on it before moving on to the main topic. For those who are new to std::ranges, a brief explanation will follow below.

#include <algorithm>
#include <array>
#include <ranges>
#include <type_traits>
#include <iostream>
#include <numeric>

int main()
{
    auto get_array_by_value = [] { return std::array{0, 1, 0, 1}; };

    // (1) a search for the max element in the temporary 
    auto dangling_iter = std::ranges::max_element(get_array_by_value());
    static_assert(std::is_same_v<std::ranges::dangling, decltype(dangling_iter)>);
    // Compilation error below: 
    // no match for 'operator*' (operand type is 'std::ranges::dangling')
    // std::cout << *dangling_iter << '\n';

    // (2) However, the code below works fine:
    std::array<int, std::size(get_array_by_value())> copied;
    auto copy_result = std::ranges::copy(get_array_by_value(), std::begin(copied));
    static_assert(std::is_same_v<std::ranges::dangling, decltype(copy_result.in)>);
    
    // (3) and this is fine, too:
    auto isOdd = [](int i){ return 0 != (i % 2);};
    auto filtered = get_array_by_value() | std::views::filter(isOdd);
    return std::accumulate(std::begin(filtered), std::end(filtered), 0);
}
  1. Here, we create a temporary array and search for its maximum element. Since the temporary is destroyed at the end of the full-expression1, a trivial implementation might have returned an iterator to an element of the destroyed array. However, std::ranges detects this and returns a special type called std::ranges::dangling. This approach accomplishes two things:
  • std::ranges::max_element is executed, any side effects are preserved (e.g. stdout output);
  • dangling_iter is impossible to dereference, preventing access to the destroyed array.
  1. A temporary is valid to be copied from, but the copy but the iterator to the end of the source range is std::ranges::dangling, which prevents dereferencing.

  2. The temporary is filtered and should be destroyed at the end of the full-expression, but the view takes ownership over it using the owning_view. This means that it is perfectly valid to iterate over the filtered range after the original temporary has been destroyed.

All the magic above may suggest that when accessing the internal state of a large object like RawImageBytes, or when creating a most-recent-N subrange of the RingBuffer, we can do better than simply returning a pointer or reference to the internal data buffer and hoping that nobody will accidentally use a dangling pointer.

The problem

Consider the MyRawImage class that holds large image data. Some computational algorithms may need image’s data as a buffer, so MyRawImage::data() method is needed to access the whole buffer. Here is the first attempt to implement it:

struct Pixel
{
    int r = 0;
    int g = 0;
    int b = 0;

    friend auto operator<=>(const Pixel& a, const Pixel& b) = default;
};

struct Metadata { /* some metadata here, width, height, etc. */ }; 

class MyRawImage
{
    std::vector<Pixel> m_buffer;
    Metadata           m_metadata;
public:
    MyRawImage(std::vector<Pixel> src) : m_buffer(std::move(src)) {}

    const Pixel& operator[](int index) const { return m_buffer[index]; }
    Pixel&       operator[](int index)       { return m_buffer[index]; }

    const std::vector<Pixel>& data() const   { return m_buffer; }
    const Metadata& information() const      { return m_metadata; }
};
Up to this point, the code appears to be following standard and conventional practices. However, I believe there is room for improvement, and that’s why:
MyRawImage loadImage(int i)
{
    return std::vector<Pixel>(i * 100, Pixel {i, i, i});
}

MyRawImage problematic(int i)
{
    std::vector<Pixel> filtered;
    auto filter = [](Pixel p) 
    { 
        p.r = std::min(p.r, 0xFF); 
        p.g = std::min(p.g, 0xFF); 
        p.b = std::min(p.b, 0xFF);
        return p; 
    };

    // oops: equivalent of `auto&& ps =  loadImage(i).data(); for (p : ps) { ... }`
    // loadImage() returns a temporary, temporary.data() reference is stored,
    // then for-loop iterates over a stored reference to a deleted temporary 
    for(Pixel p : loadImage(i).data())
        filtered.push_back(filter(p));

    return filtered;
}

std::vector<Pixel> suboptimal_performance(int i)
{
    // can't move from the temporary, even using explicit move,
    // because the reference is const
    std::vector<Pixel> filtered = loadImage(i).data();
    auto filter = [](Pixel p) 
    { 
        p.r = std::min(p.r, 0xFF); 
        p.g = std::min(p.g, 0xFF); 
        p.b = std::min(p.b, 0xFF);
        return p; 
    };
    
    for(Pixel& p : filtered)
        p = filter(p);

    return filtered;
}

Pixel fine(int i)
{
    auto max = [](auto&& range) -> Pixel 
    { 
        return *std::max_element(std::begin(range), std::end(range)); 
    };

    // this one is fine: a temporary will be destroyed after the max() calcualtion
    return max(loadImage(i).data()); 
}

int main(int, char**)
{
    constexpr static int pattern = 0x12;
    constexpr static Pixel pixelPattern = Pixel { pattern, pattern, pattern };
    auto isGood = [](const Pixel& p) { return p == pixelPattern; };

    Pixel maxPixel = fine(pattern);
    assert(maxPixel == pixelPattern);

    std::vector<Pixel> pixels = suboptimal_performance(pattern);
    assert(pixels.end() == std::ranges::find_if_not(pixels, isGood));

    MyRawImage img = problematic(pattern);
    assert(img.data().end() == std::ranges::find_if_not(img.data(), isGood));
}
There is a full source just in case some of us don’t enjoy guessing missed includes.

First, there is a performance pitfall: although moving the whole MyRawImage is possible, moving the data out of temporary is not because the constant lvalue reference prohibits even explicit std::move.

Disregarding the performance, the code above aims to accomplish the following:

  • fine() method ’loads’ image that consists of one hundred pixels with a value of {0x12,0x12,0x12} and finds the maximum among them.
  • problematic() method loads similar image and clamps every pixel’s component to the maximum value of 0xFF and ensures that no pixels deviate from the 0x12 pattern.
Program returned: 139
output.s: /app/example.cpp:77: int main(int, char**): 
         Assertion `img.data().end() == std::ranges::find_if_not(img.data(), isGood)' failed.

I believe that the reader can solve both mathematical problems mentally, but the output above suggests that until C++23 the provided code can’t. Even with C++23, there is a same pitfall with const Bar& lvalue = foo().bar();: there is no const lvalue reference to the result of foo() so its lifetime is not extended and the refererence, returned from bar() is dangling. The reader could proceed to the Temporary range expression chapter in the range-based for documentation for mode details, if needed.

The problem arises when obtaining a reference or pointer to a property of a temporary object. Often, it is permissible, but there is a risk of misuse by developers, leading to dangling references and undefined behavior.

A short STL example:

std::string getMessage() { return "I'm a long enough message to avoid SSO"; }
// fine, the string is destroyed at the semicolon
int length = strlen(getMessage().c_str());  

// bad, the string has been destroyed at the semicolon below
const char* dangling = getMessage().c_str();
int oops = strlen(dangling);    
Although it might seem syntetic example, there are some practical use cases like sending the std::string content using WinAPI SendMessage or PostMessage. While SendMessage is synchronous and fine, PostMessage will result in a dangling pointer being stored and used later.

I believe we can do better: provide an access to data() in a way that significantly reduces the likelihood of misuse and the occurrence of dangling references. We’ll try to address the performance considerations as well.

A ref-qualified member function

It could be reasonable to prevent certain member functions from being called on a temporary object. That’s what we could achieve with ref-qualified member functions. This feature is not broadly used, so here is a short description: a member function might have a reference qualifier in the same familiar way as the const qualifier. The correct overload will be chosen based on the type of *this reference. While member functions can also be const-qualified, it’s important to note that constness is an orthogonal property that does not affect the reference qualifier.

An overload resolution on ref-qualifier is done in the same way as for cv-qualifiers. To better understand how the member function overload works, let’s remember that the member function is considered to have an extra first parameter, called the implicit object parameter, which represents the object for which the member function has been called.

Thus, Foo foo; foo.f() is similar to Foo::f(foo);2, where the type of implicit argument is determined by present cv- and ref- qualifiers on the member function declaration. I think the execution result of the sample below will illustrate this much better than I can:

#include <iostream>

struct S
{
    const char* foo() const &  { return "foo: const & \n"; }
    const char* foo() &        { return "foo: & \n"; }
    const char* foo() const && { return "foo: const && \n"; }
    const char* foo() &&       { return "foo: && \n"; }

    static const char* bar(const S&)  { return "bar: const & \n"; }
    static const char* bar(S&)        { return "bar: & \n"; }
    static const char* bar(const S&&) { return "bar: const && \n"; }
    static const char* bar(S&&)       { return "bar: && \n"; }

    void cref() const & {};
    void l_ref() & {};
    void r_ref() && {};
};

int main(int, char**)
{
    auto getS = [] -> S        { return S(); };
    auto getCS = [] -> const S { return S(); };

    S s = getS();
    const S cs = getS();

    std::cout << getS().foo()    // foo: &&  
              << getCS().foo()   // foo: const && 
              << s.foo()         // foo: & 
              << cs.foo()        // foo: const & 
              << "-----------\n"
              << S::bar(getS())  // bar: && 
              << S::bar(getCS()) // bar: const && 
              << S::bar(s)       // bar: & 
              << S::bar(cs)      // bar: const & 
              << std::endl;    

    s.cref();         // ok
    getS().cref();    // ok, 'const S& self = getS();`
    
    s.l_ref();          // ok
    // getS().l_ref();  // error: 'S& self = getS()': 'S' as 'this' argument discards qualifiers [-fpermissive]

    // s.r_ref();       // error: 'S&& self = s;' requires explicit cast 
    getS().r_ref();
}

It’s worth highlighting that it’s rare to encounter a const && reference in the wild, so typically, rvalue-overloads should be non-const.

Disable a member function call for rvalues

Having read the cppreference, let’s delete undesired overloads from the MyRawImage with almost no effort:

class MyRawImage
{
    std::vector<Pixel> m_buffer;
    Metadata           m_metadata;
public:
    MyRawImage(std::vector<Pixel> src) : m_buffer(std::move(src)) {}

    const Pixel& operator[](int index) const { return m_buffer[index]; }
    Pixel&       operator[](int index)       { return m_buffer[index]; }

    const std::vector<Pixel>& data() const & { return m_buffer; }
    const std::vector<Pixel>& data() && = delete;

    const Metadata& information() const &    { return m_metadata; }
    const Metadata& information() && = delete;
};
Well, now we can’t accidentally access the property of the temporary via reference because the rvalue-this overloading is explicitly deleted. The performance is still sub-optimal because we’ve sacrificed rvalue references for the error-resistance.
<source>: In function 'MyRawImage problematic(int)':
<source>:54:36: error: use of deleted function 'const std::vector<Pixel>& MyRawImage::data() &&'
   54 |     for(Pixel p : loadImage(i).data())
      |                   ~~~~~~~~~~~~~~~~~^~

Now, let’s address the compiler’s complaints and examine the result. Currently, the only way to access MyRawImage::data() is by declaring a MyRawImage variable or using a const reference to extend the temporary lifetime. Therefore, we need to introduce a named variable or a const reference for every call to data on the temporary.

#include <ranges>
#include <cassert>
#include <vector>
#include <algorithm>

struct Pixel
{
    int r = 0;
    int g = 0;
    int b = 0;

    friend auto operator<=>(const Pixel& a, const Pixel& b) = default;
};

struct Metadata { /* some metadata here, width, height, etc. */ }; 

class MyRawImage
{
    std::vector<Pixel> m_buffer;
    Metadata           m_metadata;
public:
    MyRawImage(std::vector<Pixel> src) : m_buffer(std::move(src)) {}

    const Pixel& operator[](int index) const { return m_buffer[index]; }
    Pixel&       operator[](int index)       { return m_buffer[index]; }

    const std::vector<Pixel>& data() const & { return m_buffer; }
    const Metadata& information() const &    { return m_metadata; }
    const std::vector<Pixel>& data() && = delete;
    const Metadata& information() && = delete;
};

MyRawImage loadImage(int i)
{
    return std::vector<Pixel>(i * 100, Pixel {i, i, i});
}

MyRawImage was_problematic(int i)
{
    std::vector<Pixel> filtered;
    auto filter = [](Pixel p) 
    { 
        p.r = std::min(p.r, 0xFF); 
        p.g = std::min(p.g, 0xFF); 
        p.b = std::min(p.b, 0xFF);
        return p; 
    };

     // const lvalue reference extends temporary object lifetime
    const MyRawImage& image = loadImage(i); 
    for(Pixel p : image.data())
        filtered.push_back(filter(p));

    return filtered;
}

std::vector<Pixel> suboptimal_performance(int i)
{
    // Well, at least, now this code begs for optimization 
    MyRawImage image = loadImage(i);    // no problem, thanks to copy-elision

    // can't move from the temporary, even using explicit move,
    // because the reference is const
    std::vector<Pixel> filtered = image.data();
    auto filter = [](Pixel p) 
    { 
        p.r = std::min(p.r, 0xFF); 
        p.g = std::min(p.g, 0xFF); 
        p.b = std::min(p.b, 0xFF);
        return p; 
    };
    
    for(Pixel& p : filtered)
        p = filter(p);

    return filtered;
}

Pixel was_fine(int i)
{
    auto max = [](auto&& range) -> Pixel 
    { 
        return *std::max_element(std::begin(range), std::end(range)); 
    };

    // this one was fine: a temporary will be destroyed after the max() calcualtion
    // return max(loadImage(i).data()); 
    MyRawImage image = loadImage(i); 
    return max(image.data());    // <-- would be nice to avoid creating a named variable here
}

int main(int, char**)
{
    constexpr static int pattern = 0x12;
    constexpr static Pixel pixelPattern = Pixel { pattern, pattern, pattern };
    auto isGood = [](const Pixel& p) { return p == pixelPattern; };

    Pixel maxPixel = was_fine(pattern);
    assert(maxPixel == pixelPattern);
    
    std::vector<Pixel> pixels = suboptimal_performance(pattern);
    assert(pixels.end() == std::ranges::find_if_not(pixels, isGood));

    MyRawImage img = was_problematic(pattern);
    assert(img.data().end() == std::ranges::find_if_not(img.data(), isGood));
}
Just a few comments, as usual:

  • was_problematic method is now good, since there is no temporary access. It utilizes const lvalue reference lifetime extension: the return value is bound to a const MyRawImage& image, thus extending its lifetime to match that of the reference. So the data() call is safe;
  • on the other hand, the was_fine method had to introduce an unnecessary lvalue variable for the temporary. While it may not seem like a significant problem, it violates the principle of keeping the scope of variables as small as possible. Ideally, it would have been preferable to maintain the temporary’s lifetime within a single full-expression, but unfortunately, it didn’t work out that way;
  • and the code inside suboptimal_performance definitely looks like a candidate for optimization.

Explicitly enable the member function call on rvalues

The problem is that compiler can’t detect whether the reference to a property of the temporary will outlive the parent object. Technically there are two choices: allow the call on rvalue always or never.

But what if the developer is certain that an rvalue could be used in the specific expression? Well, we can give the ability to state this intention and forcibly allow an rvalue overload call with a little trick.

Let’s go back for a second to std::range. It does not prohibit creating a view from a temporary but performs a view type selection instead:

  • for lvalue, a non-owning std::ranges::ref_view is used;
  • for rvalue, an owning std::ranges::owning_view is used to take the ownership of the temporary and convert it to an lvalue;
  • when returning a possible-dangling iterator to a content of the temporary, return a std::ranges::dangling to indicate this.

We could adopt the last approach to our needs: the rvalue overload of data() could return a special wrapper that should not be implicitly-convertible to an underlying reference type. This approach will require the programmer’s attention and make sure that there is no dangerous access made by oversight.

Here we go:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
class MyRawImage
{
    std::vector<Pixel> m_buffer;
    Metadata           m_metadata;
public:
    // a wrapper around temporary result of MyRawImage().data() may be 
    // converted to a temporary 'std::vector<Pixel>' explictly using  allow_unsafe(). 
    // Be safe and ensure that it will not outlive its parent  MyRawImage.
    class UnsafeReference 
    {  
        std::vector<Pixel>&& m_buffer;

    public:
        UnsafeReference(std::vector<Pixel>&& buffer) : m_buffer(std::move(buffer)) {}
        UnsafeReference(const UnsafeReference&) = delete;
        UnsafeReference(UnsafeReference&&) = delete;

        // I would like it to be a free-function rather a member functions,
        // to lower the chance that the Intellisence will provide a disservice
        // to the developer slipping an unsafe getter by auto-suggestions. 
        // it's good to require a fair attention here
        friend std::vector<Pixel>&& allow_unsafe(UnsafeReference&&);
    };

    MyRawImage(std::vector<Pixel> src) : m_buffer(std::move(src)) {}

    const Pixel& operator[](int index) const { return m_buffer[index]; }
    Pixel&       operator[](int index)       { return m_buffer[index]; }

    const std::vector<Pixel>& data() const & { return m_buffer; }
    UnsafeReference data() &&                { return std::move(m_buffer); }

    const Metadata& information() const &    { return m_metadata; }
    const Metadata& information() && = delete;
};

std::vector<Pixel>&& allow_unsafe(MyRawImage::UnsafeReference&& unsafe)
{
    // 'unsafe.m_buffer' is lvalue in the function body
    return std::move(unsafe.m_buffer);
}

// ...

std::vector<Pixel> was_suboptimal(int i)
{
    // now it's good:
    // move from the temporary, because allow_unsafe returns an rvalue reference
    std::vector<Pixel> filtered = allow_unsafe(loadImage(i).data());
    auto filter = [](Pixel p) 
    { 
        p.r = std::min(p.r, 0xFF); 
        p.g = std::min(p.g, 0xFF); 
        p.b = std::min(p.b, 0xFF);
        return p; 
    };
    
    for(Pixel& p : filtered)
        p = filter(p);

    return filtered;
}

Pixel fine_again(int i)
{
    auto max = [](auto&& range) -> Pixel 
    { 
        return *std::max_element(std::begin(range), std::end(range)); 
    };

    // fine: a temporary will be destroyed after the max() calcualtion
    return max(allow_unsafe(loadImage(i).data()));
}
Now a similar approach could be implemented for the Metadata, but for now I hope the idea is clear3 to the reader, so further experiments are up to you.

The result I could live with

The complete code below:

#include <ranges>
#include <cassert>
#include <vector>
#include <algorithm>

struct Pixel
{
    int r = 0;
    int g = 0;
    int b = 0;

    friend auto operator<=>(const Pixel& a, const Pixel& b) = default;
};

struct Metadata { /* some metadata here, width, height, etc. */ }; 

class MyRawImage
{
    std::vector<Pixel> m_buffer;
    Metadata           m_metadata;
public:
    // a wrapper around temporary result of MyRawImage().data() may be 
    // converted to a temporary 'std::vector<Pixel>' explictly using  allow_unsafe(). 
    // Be safe and ensure that it will not outlive its parent  MyRawImage.
    class UnsafeReference 
    {  
        std::vector<Pixel>&& m_buffer;

    public:
        UnsafeReference(std::vector<Pixel>&& buffer) : m_buffer(std::move(buffer)) {}
        UnsafeReference(const UnsafeReference&) = delete;
        UnsafeReference(UnsafeReference&&) = delete;

        // I would like it to be a free-function rather a member functions,
        // to lower the chance that the Intellisence will provide a disservice
        // to the developer slipping an unsafe getter by auto-suggestions. 
        // it's good to require a fair attention here
        friend std::vector<Pixel>&& allow_unsafe(UnsafeReference&&);
    };

    MyRawImage(std::vector<Pixel> src) : m_buffer(std::move(src)) {}

    const Pixel& operator[](int index) const { return m_buffer[index]; }
    Pixel&       operator[](int index)       { return m_buffer[index]; }

    const std::vector<Pixel>& data() const & { return m_buffer; }
    UnsafeReference data() &&                { return std::move(m_buffer); }

    const Metadata& information() const &    { return m_metadata; }
    const Metadata& information() && = delete;
};

std::vector<Pixel>&& allow_unsafe(MyRawImage::UnsafeReference&& unsafe)
{
    // 'unsafe.m_buffer' is lvalue in the function body
    return std::move(unsafe.m_buffer);
}

MyRawImage loadImage(int i)
{
    return std::vector<Pixel>(i * 100, Pixel {i, i, i});
}

MyRawImage was_problematic(int i)
{
    std::vector<Pixel> filtered;
    auto filter = [](Pixel p) 
    { 
        p.r = std::min(p.r, 0xFF); 
        p.g = std::min(p.g, 0xFF); 
        p.b = std::min(p.b, 0xFF);
        return p; 
    };

    const MyRawImage& image = loadImage(i);  // const lvalue reference extends temporary object lifetime 
    for(Pixel p : image.data())
        filtered.push_back(filter(p));

    return filtered;
}

std::vector<Pixel> was_suboptimal(int i)
{
    // now it's good:
    // move from the temporary, because allow_unsafe returns an rvalue reference
    std::vector<Pixel> filtered = allow_unsafe(loadImage(i).data());
    auto filter = [](Pixel p) 
    { 
        p.r = std::min(p.r, 0xFF); 
        p.g = std::min(p.g, 0xFF); 
        p.b = std::min(p.b, 0xFF);
        return p; 
    };
    
    for(Pixel& p : filtered)
        p = filter(p);

    return filtered;
}

Pixel fine_again(int i)
{
    auto max = [](auto&& range) -> Pixel 
    { 
        return *std::max_element(std::begin(range), std::end(range)); 
    };

    // fine: a temporary will be destroyed after the max() calcualtion
    return max(allow_unsafe(loadImage(i).data()));
}

int main(int, char**)
{
    constexpr static int pattern = 0x12;
    constexpr static Pixel pixelPattern = Pixel { pattern, pattern, pattern };
    auto isGood = [](const Pixel& p) { return p == pixelPattern; };

    Pixel maxPixel = fine_again(pattern);
    assert(maxPixel == pixelPattern);
    
    std::vector<Pixel> pixels = was_suboptimal(pattern);
    assert(pixels.end() == std::ranges::find_if_not(pixels, isGood));

    MyRawImage img = was_problematic(pattern);
    assert(img.data().end() == std::ranges::find_if_not(img.data(), isGood));
}
Compile, run, enjoy.

Deducing ’this'

Well, there should be a place where C++23 is available, developers are careful enough when using references and probably a unicorn is hanging around. Can we do even better?

At least, we could do a mental experiment and imagine such a place.(if you're there, and if it isn't 2030s around, please, email me - I have to apply for that job)

Since C++23 the compiler is able to deduce the type of implicit member function parameter and make it explicit. Literally, it’s like converting the X x; x.foo() into X x; X::foo(x). Among other unobvious abilities, it allows the perfect forwarding of ‘*this’. There is a great overview in the Sy Brand article and a great insight in the Deducing this Patterns - talk on CppCon.

Having read the article we could drop the UnsafeReference and its explicit allow_unsafe for significantly cleaner implementation code:

class MyRawImage
{
    // ...

    template <typename Self>
    auto&& data(this Self&& self) 
    {
        return std::forward<Self>(self).m_buffer;
    }

    // ...
};
This way, the MyRawImage::data() template that captures *this as auto&& self = *this conforms to the usual forwarding reference rules: in the return statement, self is forwarded with exactly the same type as decltype(*this) was, preserving cv- and ref-qualification.

As C++23 allows usage of reference to the temporary’s member in the range-based ‘for’, there are very few possibilities to misuse the code. Thus it’s a great alternative to UnsafeReference once we assume that developers are careful enough to avoid binding getTemporary().getMemberReference() to a const lvalue reference.

Let’s take a final look at the C++23 masterpiece:

#include <ranges> 
#include <cassert>
#include <vector>
#include <algorithm>

struct Pixel
{
    int r = 0;
    int g = 0;
    int b = 0;

    friend auto operator<=>(const Pixel& a, const Pixel& b) = default;
};

struct Metadata { /* some metadata here, width, height, etc. */ };

class MyRawImage
{
    std::vector<Pixel> m_buffer;
    Metadata           m_metadata;
public:
    MyRawImage(std::vector<Pixel> src) : m_buffer(std::move(src)) {}

    const Pixel& operator[](int index) const { return m_buffer[index]; }
    Pixel& operator[](int index) { return m_buffer[index]; }

    template <typename Self>
    auto&& data(this Self&& self) 
    {
        return std::forward<Self>(self).m_buffer;
    }

    const Metadata& information() const& { return m_metadata; }
    const Metadata& information() && = delete;
};

MyRawImage loadImage(int i)
{
    return std::vector<Pixel>(i * 100, Pixel{ i, i, i });
}

MyRawImage was_problematic(int i)
{
    std::vector<Pixel> filtered;
    auto filter = [](Pixel p)
    {
        p.r = std::min(p.r, 0xFF);
        p.g = std::min(p.g, 0xFF);
        p.b = std::min(p.b, 0xFF);
        return p;
    };

    // in C++23 all the temporaries are guaranteed to be valid during 'for' 
    for (Pixel p : loadImage(i).data())
        filtered.push_back(filter(p));

    return filtered;
}

std::vector<Pixel> was_suboptimal(int i)
{
    // now it's good: move from the temporary, 
    // because the rvalue-overload of 'data()' returns an rvalue reference
    std::vector<Pixel> filtered = loadImage(i).data();
    auto filter = [](Pixel p)
    {
        p.r = std::min(p.r, 0xFF);
        p.g = std::min(p.g, 0xFF);
        p.b = std::min(p.b, 0xFF);
        return p;
    };

    for (Pixel& p : filtered)
        p = filter(p);

    return filtered;
}

Pixel fine_again(int i)
{
    auto max = [](auto&& range) -> Pixel
    {
        return *std::max_element(std::begin(range), std::end(range));
    };

    // fine: a temporary will be destroyed after the max() calcualtion
    return max(loadImage(i).data());
}

int main(int, char**)
{
    constexpr static int pattern = 0x12;
    constexpr static Pixel pixelPattern = Pixel{ pattern, pattern, pattern };
    auto isGood = [](const Pixel& p) { return p == pixelPattern; };

    Pixel maxPixel = fine_again(pattern);
    assert(maxPixel == pixelPattern);

    std::vector<Pixel> pixels = was_suboptimal(pattern);
    assert(pixels.end() == std::ranges::find_if_not(pixels, isGood));

    MyRawImage img = was_problematic(pattern);
    assert(img.data().end() == std::ranges::find_if_not(img.data(), isGood));

    // the code below is still UB, but according to the problem's specifications, 
    // the team is careful and experienced enough not to write so.
    // const auto& lvalue = loadImage(pattern).data();
    // assert(lvalue.end() == std::ranges::find_if_not(lvalue, isGood));
}
Compile once more, run, enjoy if your compiler is modern enough, and have a nice day!

Resources I’d recommend to explore

Just in case you’re here to discover that one intriguing link mentioned in the text above, there is a list of all the sources referenced:


  1. Here, at the semicolon ↩︎

  2. but without the ability to use an implocit cast or to introduce a temporary, like auto s = "char* " + std::string("string")↩︎

  3. Honestly, I hope the idea is sold ↩︎