设想这样一个场景,我们正在写一个通过代码生成markdown
的脚本
markdown
中行内代码的语法为`code`,相应显示为code
对应到编码中,我们可以创建一个InlineCode
类维护这种markdown
元素
考虑到反引号只在输出markdown
时需要,且反引号内的内容可能频繁修改
所以我们用一个string
成员变量_text
维护反引号内的内容(code),而不是整个行内代码(`code`)
而且修改内容后,我们可能还有些其他操作,比如说检查内容语法正确性啥的whatever
反正我们现在最好把_text
变成私有的,然后定义set_text
,get_formatted_text
成员函数读写内容
但这样代码的可读性就有点太好了 XD
如果我们只用暴露一个成员text
给使用者,让text
可以像一个string
一样被使用,也就是它被读写时自动调用set_text
,get_formatted_text
就好了
C#
原生支持这种代理模式,我们在C#
中可以将InlineCode
类实现如下
public class InlineCode
{
private string _text;
private void _notify(string old, string newer)
{ Console.WriteLine($"InlineCode.Text modified from `{old}` to `{newer}`");}
public string Text
{
get => $"`{_text}`";
set { _notify(_text, value); _text = value; }
}
public string Style, Dom;
// and may have other members, omit
public InlineCode() { }
};
C# Properties
在MSLearn
上的介绍
简单来说,对于一个类型为T
的C#
字段Prop
其get
必须满足委托类型(也就是函数签名+返回值类型)为T()
,set
委托也必须为void(T)
的形式
当读取Prop
时,返回get()
,当赋值Prop
时,调用set(value)
,其中value
是赋值表达式右手侧
所以上述代码中,Text
属性get
返回带反引号的内容,注意set
的value
形参的声明在语法上省略了
class Program
{
static void Main(string[] args)
{
var code = new InlineCode();
code.Text = "Rust";
Console.WriteLine(code.Text);
}
}
所以使用InlineCode
类时,只用关注内部文本内容,上述代码可以得到以下期望结果
InlineCode.Text modified from `` to `Rust`
`Rust`
现在考虑以下类似机制在C++
中的实现
第1版代码:
template <typename T> class Field {
using Getter = std::function<T()>;
using Setter = std::function<void(const T &)>;
Getter _get;
Setter _set;
public:
explicit Field( Getter getter = {}, Setter setter = {})
: _get{getter},
_set{setter} {}
T get() { return _get(); }
operator T() { return _get(); }
Field &operator=(const T &arg) {
_set(arg);
return *this;
}
};
创建代理类Field<T>
,参考C#
,将get
的类型Getter
定义为std::function<T()>
,将get
的类型Getter
定义为std::function<void(const T &)>
这样,只要分别用一个_get
和_set
变量保存Getter/Setter
然后再定义T get()
并重载它的operator T()
用于读取(_get()
),重载Field &operator=(const T &arg)
用于修改_set(arg)
在函数体中的使用:
int main() {
string str = "default";
auto quoted_getter = [&]( ) { return "`" + str + "`"; };
auto quoted_setter = [&](const string&s){ cout << "modified from `" << str << "` to `" << s << "`\n"; str = s;};
Field<string> quoted( quoted_getter, quoted_setter);
quoted = string{"Rust"};
cout << string{quoted} << endl;
}
// output:
//modified from `default` to `Rust`
//`Rust`
当然这里有个问题,using Setter = std::function<void(const T &)>;
固定了了Setter
的形参类型,我们甚至没有机会去重载一个void(T &&)
版本的Setter
用来区分实参的值类型
嗯,在C++
里左右值引用是不同的变量类型,而在有GC
的C#
上,引用并无左右引用而言
所以具体地,在main()
第6
行,我们向Field<string>::operator=(const string&)
传入了纯右值string{"Rust"}
,但函数内部不知情,不能移动string
来节省开销
所以接下来把Setter/Getter
类型而不是被代理的类型T
作为模板参数
第2版代码:
template<typename Getter, typename Setter>
class Field {
Getter _get;
Setter _set;
using GetterResT = std::invoke_result_t<Getter>;
public:
explicit Field(
Getter getter = {},
Setter setter = {}
) : _get{getter}, _set{setter} {}
GetterResT get() { return _get(); }
operator GetterResT() { return _get(); }
template<typename Arg>
Field &operator=(Arg &&arg) {
_set(std::forward<Arg>(arg));
return *this;
}
};
其中
- 使用
std::invoke_result_t<Getter>
获取_get()
返回值的类型 operator=
修改为模板函数,可以同时处理左右值了
在函数体中的使用:
int main() {
string str = "default";
auto quoted_getter = [&]( ) { return "`" + str + "`"; };
auto quoted_setter = [&](auto &&s){
cout << "modified from `" << str << "` to `" << s << "`\n";
str = std::forward<decltype(s)>(s);
};
Field quoted( quoted_getter, quoted_setter);
quoted = string{"Rust"};
cout << string{quoted} << endl;
}
// output:
//modified from `default` to `Rust`
//`Rust`
注意这里setter
(_set
)如果需要的话,定义为一个模板函数来处理左右值
..完成一半了,接下来考虑Field
作为类中成员变量如何使用
继续使用lambda
作为getter/setter
就不太可行了
因为定义类时要写明Getter/Setter
类别,但我们拿不到lambda
类型
因此改用具名的函数对象来捕获this
指针
InlineCode
类在C++
中可以实现如下
struct InlineCode {
private:
std::string _text;
struct TextGetter {
InlineCode *self;
std::string operator()() {
return "`" + self->_text + "`";
}
} ;
struct TextSetter {
InlineCode *self;
template<typename Str>
void operator()(Str &&s) {
cout << "modified from `" << self->_text << "` to `" << s << "`\n";
self->_text = std::forward<decltype(s)>(s);;
}
};
public:
Field<TextGetter, TextSetter> text;
InlineCode(): _text{}, text{{this}, {this}}{}
};
使用:
InlineCode code;
code.text = string{"Rust"};
cout << string{code.text} << endl;
看起来类的命名因为Getter/Setter
的后缀有些乱了,那再举一个栗子优化下代码结构
假设类型SomeClass
有一个属性time
,getter
返回带前缀的时间string
,setter
接收时间string
或UNIX
时间戳(long long
)
实现如下
template<typename T> using FieldAs = Field<typename T::Getter, typename T::Setter>;struct SomeClass {
std::string _time;
struct Time {
struct Getter {
SomeClass *self;
std::string operator()() {
return string{"time here is "} + self->_time;
}
};
struct Setter {
SomeClass *self;
template<typename Arg>
void operator()(Arg &&arg) {
using ValT = std::decay_t<Arg>;
if constexpr (std::is_convertible_v<ValT, std::string_view>)
self->_time = std::forward<Arg>(arg);
else {
std::tm *tm = std::localtime(&arg);
std::stringstream ss;
ss << std::put_time(tm, "%Y-%m-%d %H:%M:%S");
self->_time = ss.str();
}
}
};
};
FieldAs<Time> time;
SomeClass() : time{{this},
{this}} {};
};
首先我们定义一个模板别名
template<typename T> using FieldAs = Field<typename T::Getter, typename T::Setter>;
这样我们先在SomeClass
内部定义属性对应的类型Time
,Time
内部再定义Getter/Setter
类型,最后使用 FieldAs<Time>
定义成员变量就行,这样命名更顺眼一点
使用:
int main() {
SomeClass that;
that.time = "2023-10-02";
cout << string{that.time} << endl;
that.time = std::int64_t{1696232071};
cout << string{that.time} << endl;
}
// output:
//time here is 2023-10-02
//time here is 2023-10-02 15:34:31
Full code:
当然我认为使用Properties
模式时,Getter
和Setter
内部逻辑不能太复杂,否则整体代码可读性太差