클램프 기능이 잘못 설계됨

안녕하세요 ! 저는 Xavier Jouvenot입니다. 오늘은 C++ 표준의 std::clampstd::ranges::clamp 함수가 잘못 설계된 이유와 간단히 개선할 수 있는 방법을 알아보겠습니다.

자기 홍보:
저를 팔로우하고 프로그래머이자 작가로서의 제 작업을 확인할 수 있는 몇 가지 소셜 네트워크가 있습니다 😉
Personal blog , , GitHub

std::clamp 및 std::ranges::clamp 관련 문제



C++17부터 함수std::clamp는 함수 호출 중에 지정된 2개의 변수로 정의된 값 범위에서 변수를 유지하는 도우미로 사용할 수 있습니다. C++20에서는 함수std::ranges::clamp도 C++ 표준에 추가되었습니다.

이러한 기능은 다음과 같이 정의됩니다.

namespace std
{
template<class T>
constexpr const T& clamp( const T& v, const T& lo, const T& hi );

template<class T, class Compare>
constexpr const T& clamp( const T& v, const T& lo, const T& hi, Compare comp );

namespace ranges
{
template< class T, class Proj = std::identity,
         std::indirect_strict_weak_order<std::projected<const T*, Proj>> Comp =
             ranges::less >
constexpr const T&
   clamp( const T& v, const T& lo, const T& hi, Comp comp = {}, Proj proj = {} );
}  // namespace ranges
}  // namespace std


이러한 함수를 사용하는 코드는 다음과 같습니다.

auto value = std::clamp (variable, 0, 100);
auto other_value = std::ranges::clamp (other_variable, -100, 0);


그러나 다음 코드와 같이 보일 수도 있습니다.

auto value = std::clamp (50, 100, 75);


그리고 이 경우 이 코드 줄을 작성하는 개발자의 의도가 무엇인지 추측하기가 매우 어렵습니다.
실제로 범위의 최소값과 최대값은 코드의 실수로 인해 뒤바뀔 수 있습니다. 이는 C++ 표준에 따르면 정의되지 않은 동작입니다. 또는 이 행을 작성하는 개발자는 clamp 함수가 고정할 값 이전의 범위를 취하고 있다고 생각했습니다.

이유의 원인이 무엇이든 이 코드는 컴파일 중에 경고를 생성하지 않습니다. 이러한 종류의 오류가 눈에 띄지 않게 하는 솔루션을 쉽게 상상할 수 있습니다.

개선 가능



개발자의 삶을 더 쉽게 만들기 위해 한 가지 해결책은 clamp 함수의 디자인을 변경하는 것입니다. 즉, clamp 범위와 관련된 2개의 매개변수를 결합하는 것입니다. 예를 들어 std::pair을 사용하여 결합할 수 있습니다.

#include <cassert>
#include <functional>

namespace mystd
{
template<class T, class Compare>
constexpr const T& clamp( const T& v, const std::pair<T, T>& range, Compare comp )
{
   assert(comp(range.first, range.second));
   return comp(v, range.first) ? range.first : comp(range.second, v) ? range.second : v;
}

template<class T>
constexpr const T& clamp( const T& v, const std::pair<T, T>& range )
{
   return clamp(v, range, std::less<T>{});
}
}


그런 식으로 함수clamp를 호출할 때 개발자는 고정할 값과 범위를 명확히 해야 합니다.

auto v = mystd::clamp (5, {7, 10});


또한 범위의 최소값과 최대값이 반전되면 어설션이 트리거됩니다.

/app/example.cpp:10: constexpr const T& mystd::clamp(const T&, const std::pair<_FIter, _FIter>&, Compare) [with T = int; Compare = std::less<int>]: Assertion `comp(range.first, range.second)' failed.

std::pair 대신 구조를 사용하여 어설션을 좀 더 명시적으로 만들 수도 있습니다. 실제로 .first.second 요소를 갖는 대신 minmax 특성을 명명했을 것입니다. 그렇게 하면 다음 코드가 됩니다.

namespace mystd2
{
template<class T>
struct clamp_range
{
   T min, max;
};

template<class T, class Compare>
constexpr const T& clamp( const T& v, const clamp_range<T>& range, Compare comp )
{
   assert(comp(range.min, range.max));
   return comp(v, range.min) ? range.min : comp(range.max, v) ? range.max : v;
}

template<class T>
constexpr const T& clamp( const T& v, const clamp_range<T>& range )
{
   return clamp(v, range, std::less<T>{});
}
}

int main()
{
  auto v = mystd2::clamp (5, {10, 7});
}


다음과 같은 주장을 할 것입니다.

/app/example.cpp:32: constexpr const T& mystd2::clamp(const T&, const mystd2::clamp_range<T>&, Compare) [with T = int; Compare = std::less<int>]: Assertion `comp(range.min, range.max)' failed.


, 개발자가 문제를 만났을 때 문제를 더 쉽게 디버깅할 수 있습니다.

결론



이 기사의 제목은 어떤 사람들에게는 약간의 클릭 미끼일 수 있으며, 다른 사람들은 여기에 설명된 솔루션이 아마도 개선될 수 있다고 말할 것입니다(예: 개념 포함). 그리고 이것은 아마도 사실일 것입니다! 그러나 결론은 동일하며 표준 기능std::clamp은 개발자가 피할 수 있는 실수를 피하도록 돕기 위해 개선될 수 있고 개선되어야 합니다.

이 글에 설명된 솔루션을 실험해보고 싶다면 godbolt link 을 알려드리고, std::clamp 기능을 더욱 개선할 수 있는 방법에 대한 아이디어가 있다면 이 글 아래에 약간의 의견을 자유롭게 적어주세요 😉


이 글을 읽어주신 모든 분들께 감사드리며,
그리고 다음 글까지 화사한 하루 보내세요🙂

좋은 웹페이지 즐겨찾기