STL에는 몇가지 customization point들이 있음. (operator 는 이 글에서 논외로 침)
C++03, 11에선 begin, end, swap
C++17에 들어온 size, empty, data 등
그리고 C++20 ranges namespace에 거주하고있는 대부분의 CPO들 (quasi-customization point object들은 제외)
Customization point가 뭔지 모르고 이게 왜 필요한지도 모르는사람이 있을수 있으니 이게 대충 뭔지 짧게 설명해봄.
예를들어 C++03에 이런 클래스가 있다고 가정해보자. using이나 auto같은 키워드는 03에 있다고 가정하고 그냥 넘어가주셈 논점은 그게 아니니까
C++03에는 rvalue reference가 없음. 그래서 std::swap의 default implementation은 무조껀 복사생성자와 복사연산자를 호출함.
그래서 namespace std에 존재하고있는 모든 컨테이너들은 member function swap에서 포인터 몇개만 swap해주는식으로 구현하고 free function swap함수를 container-specific하게 overload해서 container.swap(other)을 호출하는식으로 설계가 됐었음.
근데 일개 STL 사용자들은 namespace std에 존재하는 함수에 overload 할수있는 권한이 없음. template specialization that depends on user defined type밖에 못함.
그래서 어떤 idiom이 쓰였냐면
ADL-reachable swap을 따로 정의하고 호출할땐 unqualified lookup으로 호출을 했었음. 이렇게하면 default std::swap implementation은 ADL-reachable swap이 없을때만 호출이 됨.
위 swap 같은 함수가 바로 customization point임.
begin, end도 customization point지만 qualified call to std::begin, std::end가 멤버함수 begin, end를 호출해주기도 하고 range based for loop도 멤버함수 begin, end을 호출 해줘서 구현/호출 선택권이 swap보다는 많음.
근데 swap때문에라도 이 바보같은 2-step idiom과 ADL이 어떻게 동작하는지를 이해해야됐는데 C++11에 rvalue reference 들어오고 default swap이 std::move를 사용함으로써 굳이 ADL reachable swap을 도입할필요는 거의 없어진 상태임.
여튼 C++11이 들어오면서 이 문제는 자연스럽게 자취를 감추던것 같았음. Customization point를 제외하고도 C++에 ADL로 인한 본질적인 문제는 남아있지만 std::swap의 default implementation이 효율적으로 구현됨으로써 이런 불편함은 거의 없어진 상태였으니까.
근데 2014년 말쯤에 Eric Niebler씨의 ranges 라이브러리가 표준에 제안됨. 이 라이브러는 free function customization point를 꽤나 많이 활용한 라이브러리기에 서서히 자취를 감추는거 같았던 customization point로 발생되는 문제가 다시 재조명됨.
그리고 이 ranges 라이브러리 하나때문에 call-site에서 qualified call을 해도 unqualified call을 한거와 같은 효과가 있는 customization point 구현 방법이 2015년에 표준에 제안됨. 마찬가지로 Eric Niebler씨가 고안한 방법임.
위는 Customization Point Object (CPO)라는 function object임.
생긴거부터가 골때리게 생겼는데 짧게 설명하면 customization-point를 free function 대신 global function object로 만들고 그 안에서 unqualified call을 하는 방식임. 라이브러리 개발자라면 익혀둬야할 흑마법이 하나 늘었지만
라이브러리 사용자들은:
1. customization point 함수들은 unqualified call로 호출했을때 ADL-reachable한 함수가 구현만 되있으면 type-constraint에 부합하지않아도 호출이 되니까 centralized constraint checking이 안되는 단점이 있었는데 위처럼 구현하면 qualified call이든 unqualified call이든 function object 안에서 호출되니까 이 문제점을 mitigate 할수있음.
이게 고안될 당시에 C++20 concept은 없었지만 concept-lite는 있었기에 type-constrained environment에서도 호환이 잘 되도록 설계됨.
2. Templated context에서 매번 using std::swap; swap(a, b); 와 같은 2-step idiom 을 작성 안해도 됨.
그래서 이걸로 끝나나 싶었는데 시간이 좀더 지나니까 또 새로운 문제에 직면하게됨.
CPO로 customization point의 고질적인 문제점이 해결돼서 std implementer가 아니여도 이걸 도입해보려는 3rd party library들이 최근에 하나 둘씩 늘어나고있음.
근데 CPO가 call-site에서 일어날수있는 문제점을 해결해준거지 ADL-reachable function을 제공하는 방법은 예전이랑 똑같음.
밑에 예시를 한번 봐보셈
위처럼 여러 라이브러리의 type-constraint에 부합하는 클래스를 만들었을때 별개의 라이브러리에서 다른 행동을 하지만 이름은 같은 customization point를 정의해버려놨으면 아무리 CPO를 활용한 구현이라도 뭐 어찌할 방법이 없음.
그래서 또 Eric Niebler씨가 2019년 표준에 tag_invoke란걸 제안했음.
대충 이런건데, 이 라이브러리 구현체와 똑같은 문제를 해결하려는 language feature가 지금 EWG에서 다듬어지고 있음. 순전히 내 생각이지만 아마 표준에 들어온다면 그게 들어오고 이건 기각당할 가능성이 높아보이는데, 그래도 C++23이전에서는 위 방법이 최선이기도 하고 language feature가 기각당하고 이게 들어올 가능성도 낮게나마 존재하니까 알아둬서 나쁠건 없다고 생각함.
여튼 이건 모든 라이브러리의 customization point 연결 방식을 tag_invoke 하나로 통일하고 tag_invoke함수의 첫 parameter에 타겟 CPO를 넘겨줘서 한번의 indirection을 거쳐서 호출하는 구현체임.
이걸 활용하면 중복되는 이름의 customization point도 문제없이 사용할수 있음.
위 링크에서 name collision이 있어도 잘 동작하는걸 확인할수 있음. 호출자는 tag_invoke semantic에 대해서 아무것도 알 필요도 없고 직관적이게 CPO에서 함수만 호출하면 됨.
글 쓰기 전엔 tag_invoke에 대해서만 쓰려다가 그래도 나오게 된 경위정도는 설명해야될거같아서 설명 하나 둘 붙이다보니까 글이 상당히 길어졌네.
여기서 가끔 C++20에 들어온 range 라이브러리나 executor같은거 너무 늦게 들어오는거 아니냐 위원회는 뭐하냐는 글을 몇번 봤는데 위에 설명한 free function customization point에서 생기는 문제점들때문에 지연된 이유도 있고
executor같은경우는 concurrency에 촛점을 둔 networking-ts executor, parallelism에 촛점을 둔 nvidia executor, 또 google executor랑 coroutine library 만들던 facebook사람들까지 합세해서 설계하는 와중에 over engineered돼서 산으로 갔다가 다시 좀더 lean한 API로 축소되고있는 중인거 같은데
저건 이 글에서 다룬 customization point 말고도 넘어야될 산이 좀더 많아서 지연된게 아닌가 싶음.
TLDR:
ADL customization point를 활용해서 라이브러리를 만들면:
1. 라이브러리 사용자가 매번 2-step idiom을 써야됨.
2. Centralized constraint checking이 안됨.
3. 다른 라이브러리에서 같은 이름의 다른 동작을 하는 customization point를 정의해버리면 여러 라이브러리를 사용하는 유저입장에서 상당히 골치아파짐.
라이브러리 내 모든 free function customization point들의 연결방식을 tag_invoke로 통일하고 컴파일시간이 약간 더 느려지는걸 감수하면 위에 나열된 문제들로부터 자유로워질수 있음.
오...
이런 양질의 컨텐츠... 스크랩 해두고 읽겠읍니다
으악 너무 길고 복잡해
ranges 언제 들어오냐고 불평하던 자신을 반성합니다,,