std::clamp - detect if function return value is binded to const T&
Asked Answered
H

1

7

It is better not to bind std::clamp return value to const ref, if one of its min or max parameters are rvalues.

Typical realization of std::clamp (very simplified):

template <class T>
constexpr const T& clamp(const T& value, const T& min, const T& max)
{
    return value < min ? min : max < value ? max : value;
}

And as already stated in cppreference for std::clamp there is a dangerous situation when someone write:

int n = -1;
const int& r = std::clamp(n, 0, 255);
// r is dangling

Is there any way for compile-time-detect these cases?

Hawking answered 26/1, 2017 at 9:0 Comment(9)
Is there any way to catch this with either AddressSanitizer or UndefinedBehaviorSanitizer? I've tried both and unfortunately can't seem to get them to complain.Implied
If it was not in std, you might add an deleted overload like template <typename T, typename U, typename V> constexpr std::enable_if_t<std::is_rvalue_reference<T&&>::value || std::is_rvalue_reference<U&&>::value || std::is_rvalue_reference<V&&>::value> clamp(T&& value, U&& min, V&& max) = delete;Tragedian
@Tragedian Thanks, but such a way you will delete a possibility to use int r = std::clamp(n, 0, 255);Hawking
This looks like a defect in the standard library.Seward
@vladon: You cannot check function argument according of the usage of the returned value. in both cases, it is std::clamp(n, 0, 255);.Tragedian
Why was this template function designed to return a reference in the first place?Unholy
@Unholy To avoid copy of big objects passed to it.Hawking
@Hawking I guess so, but most if not all of its typical use-cases is with numbers. Not omparing and clamping big objects....Unholy
@Hawking But they could still have overloaded for r-values, puzzles me..Intolerance
F
2

If you are willing to write your own clamp function, you can detect this and do something about it.

For example, suppose you want the clamp function to return by value if that is the only way to ensure that we don't get a dangling reference:

#include <type_traits>

template<class A, class B, class C>
constexpr std::conditional_t<
    std::is_lvalue_reference<A &&>::value && std::is_lvalue_reference<B &&>::value && std::is_lvalue_reference<C &&>::value,
    std::add_lvalue_reference_t<std::common_type_t<A, B, C>>,
    std::common_type_t<A, B, C>
> clamp(A && value, B && min, C && max)
{            
    return value < min ? min : max < value ? max : value;
}

This would make clamp(n, 0, 255) effectively have the signature int clamp(int&, int&&, int&&); you'd only get int & clamp(int&, int&, int&) if all 3 inputs are lvalue references. Making the returned reference const is trivial if you so desire.

You could also have the function fail to compile if they weren't all lvalue references:

#include <type_traits>

template<class A, class B, class C>
constexpr std::add_lvalue_reference_t<std::common_type_t<A, B, C>>
    clamp(A && value, B && min, C && max)
{
    static_assert(std::is_lvalue_reference<A &&>::value && std::is_lvalue_reference<B &&>::value && std::is_lvalue_reference<C &&>::value, "");

    return value < min ? min : max < value ? max : value;
}
Faina answered 6/2, 2017 at 17:52 Comment(1)
Makes me wonder why the hell (sorry for the language) didn't implement std::clamp this way. At least they could have prevented rvaluesIntolerance

© 2022 - 2024 — McMap. All rights reserved.