0%

Customization Point Object, tag_invoke and future

namespace

由於繼承自C語言,所以會遇到像這樣的問題

1
2
3
4
5
6
// my_std.h
void foo(int);
void bar(void);
// other_lib.h
int foo(void);
int baz(int, int);

來自於不同的Library,且提供不同的實作,在使用上會出現一些問題
而C語言時代的解法就是對Function Name加料

1
2
3
4
5
6
// my_std.h
void my_std_foo(int);
void my_std_bar(void);
// other_lib.h
int other_lib_foo(void);
int other_lib_baz(int, int);

而C++做的事情差不多,用namespace隔開

1
2
3
4
5
6
7
8
9
10
// my_std.h
namespace my_std {
void foo(int);
void bar(void);
}
// other_lib.h
namespace other_lib {
int foo(void);
int baz(int, int);
}

ADL

全名是Argument-Dependent Lookup
只要有一個參數在函數的命名空間內,在使用的時候就不用加namespace prefix
在ADL只關心函數,不包含Function Object,這點在之後會用到

1
2
3
4
5
6
7
8
9
10
11
12
13
namespace A
{
struct Empty {};
void foo(int) {}
void bar(Empty, int) {}
}

void func()
{
A::foo(2);
bar(A::Empty{}, 1);
std::cout << 1; // operator<< (std::cout, 1) Due to ADL
}

如果沒有ADL,最後那行只能這樣寫了

1
std::operator<<(std::cout, 1);

應該沒人會喜歡

Example for std::swap

這是拓展問題的最好範例

1
2
3
4
5
6
7
8
namespace std {
template<typename T>
void swap(T& a, T& b) {
T temp(a);
a = b;
b = temp;
}
}

如果我們要對自己的class做swap動作時,該怎麼做

1
2
3
4
5
6
namespace My {
class A {
public:
void swap(A&) {}
};
}

直覺的寫法可以這樣做

1
2
3
4
5
6
```cpp
namespace std
{
template<>
void swap<::My::A>(::My::A& a, ::My::A& b) {a.swap(b);}
}

這樣寫是Undefined Beahvior
而另外一種做法是

1
2
template<>
void std::swap<My::A>(My::A& a, My::A& b) { a.swap(b); }

不過如果是My::A<T>的話就不管用了
而比較常用的手法,就是利用ADL

1
2
3
4
5
6
7
8
9
10
11
12
void fun(...); // 1 
namespace My {
struct A{};
void fun(const A&); // 2
}
namespace Code {
void fun(int); // 3
void use() {
::My::A a;
fun(a); // HERE
}
}

呼叫的foo(a)時,會考慮2和3,1是因為在Code的namespace已經找到一個fun了,部會在往上層的scope去尋找
利用ADL two-step的手法來拓展我們的std::swap

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
#include <utility>
namespace My
{
struct A
{
friend void swap(A& a, A& b) { a.swap(b); }
};
template<typename T>
struct B
{
friend void swap(B& a, B& b) { a.swap(b); }
};
}
namespace Code
{
void use()
{
using std::swap;
::My::A a1, a2;
swap(a1, a2); // HERE #1
::My::B<int> b1, b2;
swap(b1, b2); // HERE #2
int i1, i2;
swap(i1, i2); // NOPE
}
}

在這個範例當中,呼叫swap的時候沒加上namespace,而讓std::swap注入當今的Scope下,如果可以透過ADL找到對應的函數,則用特化版的函數,不然就用原先的std::swap做預設值

Drawback on ADL two-step

最大的問題在於

1
2
using std::swap;
swap(a1, a2);

可能一不小心就寫成

1
std::swap(a1, a2);

不會報錯,頂多是效能差
另外一個比較大的問題是這個

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
namespace __my_std_impl
{
template<typename T>
auto __distance_impl(T first, T last) {/* ... */}
template<typename T>
auto distance(T first, T last) {return __distance_impl(first, last);}
}
struct incomplete;
template<typename T> struct box {T value;};
void use()
{
incomplete* i = nullptr; // fine
__my_std_impl::distance(i, i); // fine
box<incomplete>* b = nullptr; // fine
__my_std_impl::distance(b, b); // !!!
}

__my_std_impl::distance(b, b)的地方會報錯
原因在於__distance_impl階段會進行ADL動作,在box的定義上尋找是否有__distance_impl的函數,因找到incomplete value,故報錯
一種可能的解法就是加上namespace

1
2
template<typename T>
auto distance(T first, T last) {return __my_std_impl::__distance_impl(first, last);}

Customization Point Object

兩階段ADL的最大問題就是容易誤用
因此叫Standlard library來幫你做這件事
其中最簡單的CPO就長這樣

1
2
3
4
5
6
namespace std::ranges {
inline constexpr swap = [](auto& a, auto& b) {
using std::swap;
swap(a, b);
};
}

這裡的swap是個constexpr object,而不是個function,不過他是一個functor,因此可以適用於所有std::swap的環境下
CPO還有一個優勢,它是一個object,所以它能夠這樣用

1
some_ranges | views::transform(ranges::begin)

1
some_ranges | views::transform(std::begin)

這樣用不合法,因為它是個template function

Niebloids

Niebloids是要解決另外一個問題,去除掉不想要的ADL candicate
禁用的方法就是讓它成為CPO
以下是StackOverflow的範例

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
#include <iostream>
#include <type_traits>
namespace mystd
{
class B{};
class A{};
template<typename T>
void swap(T &a, T &b)
{
std::cout << "mystd::swap\n";
}
}

namespace sx
{
namespace impl {
//our functor, the niebloid
struct __swap {
template<typename R, typename = std::enable_if_t< std::is_same<R, mystd::A>::value > >
void operator()(R &a, R &b) const
{
std::cout << "in sx::swap()\n";
// swap(a, b);
}
};
}
inline constexpr impl::__swap swap{};
}

int main()
{
mystd::B a, b;
swap(a, b); // calls mystd::swap()

using namespace sx;
mystd::A c, d;
swap(c, d); //No ADL!, calls sx::swap!

return 0;
}

如果找到的是function object,則不會使用ADL

tag_invoke

根據libunifex裡面的描述,一樣是透過ADL,要解決以下兩個問題

  1. Each one internally dispatches via ADL to a free function of the same name, which has the effect of globally reserving that identifier (within some constraints). Two independent libraries that pick the same name for an ADL customization point still risk collision.
  2. There is occasionally a need to write wrapper types that ought to be transparent to customization. (Type-erasing wrappers are one such example.) With C++20’s CPOs, there is no way to generically forward customizations through the transparent wrap
    比較大的問題是第一點,由於透過ADL尋找函數,所以每個namespace下都需要將函數名稱當作保留字
1
2
3
4
5
6
7
8
9
10
11
12
namespace std::range {
inline constexpr swap = [](auto& a, auto& b) {
using std::swap;
swap(a, b);
};
}
namespace A {
void swap(...);
}
nameapce B {
void swap(....);
}

也就是你用了swap當CPO之後,其他地方都要保留swap當作保留字不能使用,tag_invoke就是為了這點而生的
參考C++11 tag_invoke的實作 duck_invoke

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
#include <bfg/tag_invoke.h>
namespace compute {
BFG_TAG_INVOKE_DEF(formula);
} // namespace compute

template <typename Compute>
float do_compute(const Compute & c, float a, float b)
{
return compute::formula(c, a, b);
}

struct custom_compute
{
private:
friend float
tag_invoke(compute::formula_t, const custom_compute &, float a, float b)
{
return a * b;
}
};

int main()
{
do_compute(custom_compute{}, 2, 3);
}

主要的作法是

  • 需要一個CPO參數,以上的範例是formula
  • 只需要一個tag_invoke function,不過可以Overloading,對不同的CPOj做不同的處理
    不過tag_invoke製造了其他問題,難以理解且囉嗦

Future

由於Executors跳票了,所以tag_invoke也不一定是最終解決方案
目前有其他提案,不過會不會被接受也在未定之天
詳細可以找找P2547R0來研究

Reference

如何理解 C++ 中的 定制点对象 这一概念?为什么要这样设计?
c++ execution 与 coroutine (一) : CPO与tag_invoke
C++特殊定制:揭秘cpo与tag_invoke!
Customization Points
Argument-dependent lookup - cppreference.com
Why tag_invoke is not the solution I want (brevzin.github.io)
What is a niebloid?
ADL,Concepts与扩展C++类库带来的思考
Duck Invoke — tag_invoke for C++11