2402C++,偷窥C++未来的反射

原文

在此体验.
只需要包含#include <experimental/meta>.

#include <experimental/meta>
int main() {constexpr auto r = ^int;typename[:r:] x = 42;// ==: int x = 42;typename[:^char:] c = '*';// ==: char c = '*';
}

选择成员

这是一个操作成员的小示例:

struct S { unsigned i:2, j:6; };
consteval auto member_number(int n) {if (n == 0) return ^S::i;else if (n == 1) return ^S::j;
}
int main() {S s{0, 0};s.[:member_number(1):] = 42;//等同于:`s.j=42`;s.[:member_number(5):] = 0;//错误`(member_number(5)`不是常量).
}

通过提升(reflection)operator先返回meta::info反射类型,再通过splicing重新得到成员类型,从而访问成员.

类型列表转类型大小列表:

constexpr std::array types = {^int, ^float, ^double};//这里需要`consteval`,因为尚未实现`consteval`传播`(P2564)`constexpr std::array sizes = []() consteval {std::array<std::size_t, types.size()> r;std::transform(types.begin(), types.end(), r.begin(), std::meta::size_of);return r;
}();
static_assert(sizes[0] == sizeof(int));
static_assert(sizes[1] == sizeof(float));
static_assert(sizes[2] == sizeof(double));

该示例同样很简单,不多讲,最终大小的内容就相当于:

std::array<std::size_t, 3> sizes = {sizeof(int), sizeof(float), sizeof(double)};

造整序

通过反射简化实现make_integer_sequence:

template<typename T>
consteval std::meta::info make_integer_seq_refl(T N) {std::vector args{^T};for (T k = 0; k < N; ++k) {args.push_back(std::meta::reflect_value(k));}return substitute(^std::integer_sequence, args);
}
template<typename T, T N>using make_integer_sequence = [:make_integer_seq_refl<T>(N):];
static_assert(std::same_as<make_integer_sequence<int, 10>,std::make_integer_sequence<int, 10>>);

该实现逻辑也比较清晰,主要涉及两个reflect_valuesubstitude元函数.
其中,reflect_value的声明为:

namespace std::meta {template<typename T>consteval auto reflect_value(T const&)->info;template<typename R>consteval auto reflect_values(R const&)->std::span<info>;
}

这两个元函数来把Constantvalue(s)提升(meta::info)反射类型表示,如:

constexpr std::vector<int> v{ 1, 2, 3 };
constexpr std::span<std::meta::info> rv = reflect_values(v);

随后,便可把该提升序列重新Splicing出来,如按模板参数使用:

std::integer_sequence<int, ...[:rv:]...> is123;
//与std::integer_sequence<int,1,2,3>相同

以上仅是示例,EDGReflection尚不支持reflect_values,只支持reflect_value.

因此,

args.push_back(std::meta::reflect_value(k));

的意思,就是生成一个常数序列,再通过生成的序列创建一个std::integer_sequence,需要用到如下标准声明的substitute元函数:

namespace std::meta {consteval auto substitute(info templ, std::span<info> args)->info { ... };
}

功能是根据已有类型,参数,生成新的类型.一例:

using namespace std::meta;
template<typename ... Ts> struct X {};
template<> struct X<int, int> {};
constexpr info type = ^X<int, int, float>;
constexpr info templ = template_of(type);
constexpr span<info> args = template_arguments_of(type);
constexpr info new_type = substitute(templ, args.subspan(0, 2));typename[:new_type:] xii; //`X<int,int>`类型来选择`特化`.不能实例化`显式/部分特化`取代的`主模板定义`. 

根据X<int,int,float>生成了新的X<int,int>类型.
但是,EDG目前有些局限,它使用std::vector<info>来代替std::span<info const>,因此

substitute(^std::integer_sequence, args);

中才使用std::vector<info>参数.

用反射来取类布局信息:

struct member_descriptor
{std::size_t offset;std::size_t size;bool operator==(member_descriptor const&) const = default;
};
//返回`std::array<member_descriptor,N>`
template <typename S>
consteval auto get_layout() {constexpr size_t N = []() consteval {return nonstatic_data_members_of(^S).size();}();std::array<member_descriptor, N> layout;[: expand(nonstatic_data_members_of(^S)) :] >> [&, i=0]<auto e>() mutable {layout[i] = {.offset=offset_of(e), .size=size_of(e)};++i;};return layout;
}
struct X
{char a;int b;double c;
};
constexpr auto Xd = get_layout<X>();
static_assert(Xd.size() == 3);
static_assert(Xd[0] == member_descriptor{.offset=0, .size=1});
static_assert(Xd[1] == member_descriptor{.offset=4, .size=4});
static_assert(Xd[2] == member_descriptor{.offset=8, .size=8});

get_layout()主要逻辑点,来取类型非静态数据成员信息,在member_descriptor里面保存信息.

因为EDG目前不支持扩展语句,所以增加了一些实现的复杂度.如果使用扩展语句,核心语句实现可这样:

std::array<member_descriptor, N> layout;
int i = 0;
template for (constexpr auto e : std::meta::nonstatic_data_members_of(^S)) {layout[i] = {.offset=offset_of(e), .size=size_of(e)};++i;
}

expand()EDG扩展语句临时平替,实现为:

namespace __impl {template<auto... vals>struct replicator_type {template<typename F>constexpr void operator>>(F body) const {(body.template operator()<vals>(), ...);}};template<auto... vals>replicator_type<vals...> replicator = {};
}
template<typename R>
consteval auto expand(R range) {std::vector<std::meta::info> args;for (auto r : range) {args.push_back(reflect_value(r));}return substitute(^__impl::replicator, args);
}

示例中其他使用的元函数皆这样,逻辑清晰,不必多讲.

枚举到串

最经典的示例,相当于反射界的你好,世界.
最经典的当属标准版本:

template <typename E>requires std::is_enum_v<E>
constexpr std::string enum_to_string(E value) {template for (constexpr auto e : std::meta::members_of(^E)) {if (value == [:e:]) {return std::string(std::meta::name_of(e));}}return "<unnamed>";
}
enum Color { red, green, blue };
static_assert(enum_to_string(Color::red) == "red");
static_assert(enum_to_string(Color(42)) == "<unnamed>");

反操作版本:

template <typename E>requires std::is_enum_v<E>
constexpr std::optional<E> string_to_enum(std::string_view name) {template for (constexpr auto e : std::meta::members_of(^E)) {if (name == std::meta::name_of(e)) {return [:e:];}}return std::nullopt;
}

但是EDG不支持扩展语句,所以使用expand()代替:

template<typename E>requires std::is_enum_v<E>
constexpr std::string enum_to_string(E value) {std::string result = "<unnamed>";[:expand(std::meta::enumerators_of(^E)):] >>[&]<auto e>{if (value == [:e:]) {result = std::meta::name_of(e);}};return result;
}
enum Color { red, green, blue };
static_assert(enum_to_string(Color::red) == "red");
static_assert(enum_to_string(Color(42)) == "<unnamed>");

该实现的复杂度为O(N),他们提供了另一个利用Ranges算法只需要O(log(N))复杂度的实现:

template <typename E>requires std::is_enum_v<E>
constexpr std::string enum_to_string(E value) {constexpr auto enumerators =std::meta::members_of(^E)| std::views::transform([](std::meta::info e){return std::pair<E, std::string>(std::meta::value_of<E>(e), std::meta::name_of(e));})| std::ranges::to<std::map>();auto it = enumerators.find(value);if (it != enumerators.end()) {return it->second;} else {return "<unnamed>";}
}

这样借助std::map来实现.

实现元组

与传统递归继承实现相比,更简单的Tuple实现法:

namespace std::meta {consteval auto make_nsdm_description(info type, nsdm_options options = {}) {return nsdm_description(type, options);}
}
template<typename... Ts> struct Tuple {struct storage;static_assert(is_type(define_class(^storage, {make_nsdm_description(^Ts)...})));storage data;Tuple(): data{} {}Tuple(Ts const& ...vs): data{ vs... } {}
};
template<typename... Ts>struct std::tuple_size<Tuple<Ts...>>: public integral_constant<size_t, sizeof...(Ts)> {};
template<std::size_t I, typename... Ts>
struct std::tuple_element<I, Tuple<Ts...>> {static constexpr std::array types = {^Ts...};using type = [: types[I] :];
};
consteval std::meta::info get_nth_nsdm(std::meta::info r, std::size_t n) {return nonstatic_data_members_of(r)[n];
}
template<std::size_t I, typename... Ts>constexpr auto get(Tuple<Ts...> &t) noexcept -> std::tuple_element_t<I, Tuple<Ts...>>& {return t.data.[:get_nth_nsdm(^decltype(t.data), I):];}
template<std::size_t I, typename... Ts>constexpr auto get(Tuple<Ts...> const&t) noexcept -> std::tuple_element_t<I, Tuple<Ts...>> const& {return t.data.[:get_nth_nsdm(^decltype(t.data), I):];}
template<std::size_t I, typename... Ts>constexpr auto get(Tuple<Ts...> &&t) noexcept -> std::tuple_element_t<I, Tuple<Ts...>> && {return std::move(t).data.[:get_nth_nsdm(^decltype(t.data), I):];}
int main() {auto [x, y, z] = Tuple{1, 'c', 3.14};assert(x == 1);assert(y == 'c');assert(z == 3.14);
}

这样实现的关键在于生成代码,而EDG当前并不支持注入源码,所以提供了丐版std::meta::nsdm_descriptionstd::meta::define_class替代元函数,来允许合成简单的struct/union类型.声明为:

namespace std::meta {struct nsdm_options_t {optional<string_view> name;optional<int> alignment;optional<int> width;};consteval auto nsdm_description(info type, nsdm_options options = {}) -> info;consteval auto define_class(info class_type, span<info const>) -> info;
}

nsdm_description返回给定类型非静态数据成员的反射描述信息,nsdm_options_t指定数据成员的比如名,对齐和宽度额外信息,
define_class接受一个不完整的class/struct/union非静态数据成员的反射元信息序列(由nsdm_description的返回值构成),把这些非静态数据成员注入生成类型里面.

这就是注入源码的基本能力,弱化版的实现.
如:

template<typename T> struct S;
constexpr auto U = define_class(^S<int>, {nsdm_description(^int, {.name="i", .align=64}),nsdm_description(^int, {.name="j", .align=64}),
});
// S<int> ==等价于.
// template<> struct S<int> {
//   alignas(64) int i;
//   alignas(64) int j;
// };

S自动生成的非静态数据成员,如果不指定nsdm_options_t,则生成的数据成员名默认为_0,_1,_2....
回到Tuple的实现,传统方法一个是递归继承,一个是递归复合,实现后者时有许多问题,因此一般利用前者实现.

而利用反射生成代码能力,可直接合成一个storage内部类,把所有Tuple元素全部注入到该内部类当中,便可轻易地生成一个Tuple类.

借助反射,很容易实现std::tuple_element:

template<std::size_t I, typename... Ts>
struct std::tuple_element<I, Tuple<Ts...>> {static constexpr std::array types = {^Ts...};using type = [: types[I] :];
};

std::get的实现同样简单:

consteval std::meta::info get_nth_nsdm(std::meta::info r, std::size_t n) {return nonstatic_data_members_of(r)[n];
}
template<std::size_t I, typename... Ts>constexpr auto get(Tuple<Ts...> &t) noexcept -> std::tuple_element_t<I, Tuple<Ts...>>& {return t.data.[:get_nth_nsdm(^decltype(t.data), I):];}

通过反射,可直接操作类型元信息,不再需要额外的奇技淫巧递归取这些信息.

构到构数组

这也是一个生成代码的示例:

namespace std::meta {consteval auto make_nsdm_description(info type, nsdm_options options = {}) {return nsdm_description(type, options);}
}template <typename T, std::size_t N>
struct struct_of_arrays_impl;
consteval auto make_struct_of_arrays(std::meta::info type,std::meta::info N) -> std::meta::info {std::vector<std::meta::info> old_members = nonstatic_data_members_of(type);std::vector<std::meta::nsdm_description> new_members = {};for (std::meta::info member : old_members) {auto array_type = substitute(^std::array, {type_of(member), N });auto mem_descr = make_nsdm_description(array_type, {.name = name_of(member)});new_members.push_back(mem_descr);}return std::meta::define_class(substitute(^struct_of_arrays_impl, {type, N}),new_members);
}
template <typename T, size_t N>
using struct_of_arrays = [: make_struct_of_arrays(^T, ^N) :];
struct point {float x;float y;float z;
};
int main() {using points = struct_of_arrays<point, 2>;points p = {.x={1.1, 2.2},.y={3.3, 4.4},.z={5.5, 6.6}};static_assert(p.x.size() == 2);static_assert(p.y.size() == 2);static_assert(p.z.size() == 2);for (size_t i = 0; i != 2; ++i) {std::cout << "p[" << i << "] = (" << p.x[i] << ", " << p.y[i] << ", " << p.z[i] << ")\n";}
}
//输出:
//p[0]=(1.1,3.3,5.5)
//p[1]=(2.2,4.4,6.6)

把当前结构类型的所有非静态数据成员取出来,再根据这些信息重新生成数组形式的成员.
最后生成的points相当于:

using points = struct_of_arrays<point, 2>;
// struct points {
//   std::array<float, 2> x;
//   std::array<float, 2> y;
//   std::array<float, 2> z;
// };

解析命令行选项

再来看一个利用反射仿Rustclap(CommandLineArgumentParser)的实现,clapRust命令行参数解析器.
最终效果为:

struct Args : Clap {Option<std::string, {.use_short=true, .use_long=true}> name;Option<int, {.use_short=true, .use_long=true}> count = 1;
};
int main(int argc, char** argv) {auto opts = Args{}.parse(argc, argv);for (int i = 0; i < opts.count; ++i) { //`opts.count`的类型为`int`.std::print("Hello {}!", opts.name); //`opts.name`类型为`std::string`}
}

示例中定制的Args支持两个参数,一个是name,一个是有默认值的count.如果编译参数为:

./test -n WG21 -c 7

-n就对应于name,-c对应于count.则输出结果将为:

Hello WG21!
Hello WG21!
Hello WG21!
Hello WG21!
Hello WG21!
Hello WG21!
Hello WG21!

可在Args中定制自己的参数列表,在Clap封装了所有的解析操作.

实现此效果,先要定义FlagsOption.

struct Flags {bool use_short;bool use_long;
};
template <typename T, Flags flags>
struct Option {std::optional<T> initializer;Option() = default;Option(T t) : initializer(t) { }static constexpr bool use_short = flags.use_short;static constexpr bool use_long = flags.use_long;
};

Flags来表示参数,比如短形式为-n,长形式就为--name,可根据不同方式来解析.Option来表示定制的可选参数,有两个构造器,表示可选初化参数值.

比如只写./test -n WG21,此时count提供默认初化为1,从而简化参数.
接着,定义Clap的解析方式:

struct Clap {template <typename Spec>auto parse(this Spec const& spec, int argc, char** argv) {//...}
};

这里使用了C++23推导本作为定制点的表示方式,从而简化传统的CRTP方式.把argcargv传递进来,下一步操作:

template <typename Spec>
auto Clap::parse(this Spec const& spec, int argc, char** argv) {std::vector<std::string_view> cmdline(argv + 1, argv + argc);//检查`cmdline`是否包含`--help`等.struct Opts;static_assert(is_type(spec_to_opts(^Opts, ^Spec)));Opts opts;//...

如果参数列表为./test -n WG21 -c 7,则除了第一个参数,剩余的实际参数都保存到cmdline中,所以cmdline的大小为4.

接着开始解析,先通过生成代码自动生成Opts类,该类作为解析的结果,也就是auto opts=Args{}.parse(argc,argv);中的opts类型.

根据用户自定义的Args类中的非静态数据成员自动生成该返回类型,生成后的结构为:

struct Opts { std::string name; int count; };

通过spec_to_opts来生成,实现为:

consteval auto spec_to_opts(std::meta::info opts, std::meta::info spec) -> std::meta::info {std::vector<std::meta::nsdm_description> new_members;for (auto member : nonstatic_data_members_of(spec)) {auto new_type = template_arguments_of(type_of(member))[0];new_members.push_back(make_nsdm_description(new_type, {.name=name_of(member)}));}return define_class(opts, new_members);
}

逻辑不算复杂,就是使用前面nsdm_descriptiondefine_class来生成简单类型的代码.

因为不支持扩展语句,因此下一步要借助新Z类型expand()遍历参数.

template <typename Spec>
auto Clap::parse(this Spec const& spec, int argc, char** argv) {//...struct Z {std::meta::info spec;std::meta::info opt;};[:std::meta::expand([]() consteval {auto spec_members = nonstatic_data_members_of(^Spec);auto opts_members = nonstatic_data_members_of(^Opts);std::vector<Z> v;for (size_t i = 0; i != spec_members.size(); ++i) {v.push_back({.spec=spec_members[i], .opt=opts_members[i]});}return v;}()):] >> [&]<auto Z>{//...}

Z包含两个分别保存ArgsOpts的非静态数据成员信息的成员,当前示例它的大小为2.每一组信息就对应一个参数,2个分别对应-n-c.

如果用扩展语句写,逻辑会更加清晰,对应写法为:

template for (constexpr auto [sm, om] : std::views::zip(nonstatic_data_members_of(^Spec),nonstatic_data_members_of(^Opts))) {//...
}

具体处理每一组参数的逻辑如下:

template <typename Spec>
auto Clap::parse(this Spec const& spec, int argc, char** argv) {//...>> [&]<auto Z>{constexpr auto sm = Z.spec;constexpr auto om = Z.opt;auto& cur = spec.[:sm:];//查找与此选项关联的参数auto it = std::find_if(cmdline.begin(), cmdline.end(),[&](std::string_view arg){return cur.use_short && arg.size() == 2 && arg[0] == '-' && arg[1] == name_of(sm)[0]|| cur.use_long && arg.starts_with("--") && arg.substr(2) == name_of(sm);});if (it == cmdline.end()) {//无此参数if constexpr (has_template_arguments(type_of(om)) && template_of(type_of(om)) == ^std::optional) {//`类型`是可选的,所以参数也是return;} else if (cur.initializer) {//类型不是可选的,但提供了一个可用的初化器.opts.[:om:] = *cur.initializer;return;} else {std::cerr << "Missing required option " << name_of(sm) << '\n';std::exit(EXIT_FAILURE);}} else if (it + 1 == cmdline.end()) {std::cout << "Option " << *it << " for " << name_of(sm) << " is missing a value\n";std::exit(EXIT_FAILURE);}//好了,找到了参数,试解析一下auto iss = std::ispanstream(it[1]);if (iss >> opts.[:om:]; !iss) {std::cerr << "Failed to parse " << it[1] << " into option " << name_of(sm)<< " of type " << name_of(type_of(om))<< '\n';std::exit(EXIT_FAILURE);}};return opts;
}

整体实现思路就是,在cmdline参数列表中,根据cur中的信息查找,如果未查到,则看参数是否可选,有默认可选值的,就把该值读取出来,保存到opts中;

如果查找到的位置后面没有紧跟参数值,如-n后面什么也没有,则缺少参数值.

如果找到了参数,则使用C++23std::ispanstream把值读取到opts返回值当中,it查找到的位置为参数的位置,参数位置后面的it[1]就是参数值的位置.
如此便借助反射实现了一个可定制的Clap,逻辑还是比较清晰的,但受限于当前的实现,绕了一些弯路,稍微麻烦了一些.

完整实现为:

//库
namespace clap {struct Flags {bool use_short;bool use_long;};template <typename T, Flags flags>struct Option {std::optional<T> initializer;Option() = default;Option(T t) : initializer(t) { }static constexpr bool use_short = flags.use_short;static constexpr bool use_long = flags.use_long;};consteval auto spec_to_opts(std::meta::info opts, std::meta::info spec) -> std::meta::info {std::vector<std::meta::nsdm_description> new_members;for (auto member : nonstatic_data_members_of(spec)) {auto new_type = template_arguments_of(type_of(member))[0];new_members.push_back(make_nsdm_description(new_type, {.name=name_of(member)}));}return define_class(opts, new_members);}struct Clap {template <typename Spec>auto parse(this Spec const& spec, int argc, char** argv) {std::vector<std::string_view> cmdline(argv + 1, argv + argc);//检查`cmdline`是否包含`--help`等.struct Opts;static_assert(is_type(spec_to_opts(^Opts, ^Spec)));Opts opts;struct Z {std::meta::info spec;std::meta::info opt;};[:std::meta::expand([]() consteval {auto spec_members = nonstatic_data_members_of(^Spec);auto opts_members = nonstatic_data_members_of(^Opts);std::vector<Z> v;for (size_t i = 0; i != spec_members.size(); ++i) {v.push_back({.spec=spec_members[i], .opt=opts_members[i]});}return v;}()):] >> [&]<auto Z>{constexpr auto sm = Z.spec;constexpr auto om = Z.opt;auto& cur = spec.[:sm:];//查找与此选项关联的参数auto it = std::find_if(cmdline.begin(), cmdline.end(),[&](std::string_view arg){return cur.use_short && arg.size() == 2 && arg[0] == '-' && arg[1] == name_of(sm)[0]|| cur.use_long && arg.starts_with("--") && arg.substr(2) == name_of(sm);});if (it == cmdline.end()) {//无此参数if constexpr (has_template_arguments(type_of(om)) && template_of(type_of(om)) == ^std::optional) {//类型是可选的,所以参数也是return;} else if (cur.initializer) {//该类型不是可选的,但提供了一个可用初化器.opts.[:om:] = *cur.initializer;return;} else {std::cerr << "Missing required option " << name_of(sm) << '\n';std::exit(EXIT_FAILURE);}} else if (it + 1 == cmdline.end()) {std::cout << "Option " << *it << " for " << name_of(sm) << " is missing a value\n";std::exit(EXIT_FAILURE);}//好了,找到了参数,试解析一下auto iss = std::ispanstream(it[1]);if (iss >> opts.[:om:]; !iss) {std::cerr << "Failed to parse " << it[1] << " into option " << name_of(sm)<< " of type " << name_of(type_of(om))<< '\n';std::exit(EXIT_FAILURE);}};return opts;}};
}

小结

若按100%来谈论反射的进度,前两年更新时进度大概在20%-30%,而如今大概到了30%-40%.从本文也可见已更加完善了实现,也全部支持最新语法,其他相关的反射特性也有了平替丐版实现,虽说还不够简便,也缺少很多功能,但至少能用了.

我想C++反射也是要分几次标准才能真正完善,进度到60%大概可第一次进标准,也就是进C++26.此时会缺少注入源码该强特性,及自定义属性这类辅助特性,只会包含最基本反射特性.

即使如此,也敲开C++第三阶段元编程大门,绝对会是一个强大的C++新纪元,产生式元编程也会更加流行.

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/687564.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

渲染案例 |《甲辰春来,福暖四季》蓝海创意云助力央视新闻频道打造2024龙年除夕视觉盛宴

随着2024年甲辰龙年的脚步渐近&#xff0c;中央广播电视总台新闻频道精心策划的除夕特别节目《甲辰春来&#xff0c;福暖四季》于2月9日上午9点准时与全国观众见面。这一场充满传统韵味与现代气息的视觉盛宴&#xff0c;不仅展现了浓厚的节日氛围&#xff0c;更在技术上实现了突…

Halcon 元组/数组基本操作

Halcon 元组/数组基本操作 ** 元组/数组 tuple *******数组创建与字典******* ** 创建一个数组 A : [1,3,45,6] A[0] : 1 A[1] : 2** 定义一个key value字典类型create_dict (DictHandle) set_dict_tuple (DictHandle, test_tuple, A) set_dict_tuple (DictHandle, test_obj, …

Could not load request class : org.hibernate.dialect.MariaDB102Dialect

最近给项目做了点小改动&#xff1a;升级到了Spring Boot 3.1.5&#xff0c;然后它就出现了这个报错&#xff1a;Could not load request class : org.hibernate.dialect.MariaDB102Dialect。 解决办法&#xff1a;将JPA dialect改成org.hibernate.dialect.MariaDBDialect。

[HTML]Web前端开发技术26(HTML5、CSS3、JavaScript )JavaScript基础——喵喵画网页

希望你开心&#xff0c;希望你健康&#xff0c;希望你幸福&#xff0c;希望你点赞&#xff01; 最后的最后&#xff0c;关注喵&#xff0c;关注喵&#xff0c;关注喵&#xff0c;佬佬会看到更多有趣的博客哦&#xff01;&#xff01;&#xff01; 喵喵喵&#xff0c;你对我真的…

基于STM32技术的智慧农业系统

基于STM32技术的智慧农业系统研究 摘要: 随着物联网技术的飞速发展,智慧农业逐渐成为实现农业现代化的重要手段。本文介绍了一种基于STM32技术的智慧农业系统,详细阐述了系统的硬件设计、软件编程和功能实现,并通过实验验证了系统的可行性和有效性。 关键词:STM32;智慧…

Java数字孪生智慧工地数据大屏APP项目源码

目录 智慧工地云平台核心功能 1.劳务管理 2.视频监控 3.安全教育 4.进度管理 5.环境监测 6.塔吊监控 7.升降机监控 8.工地广播 9.深基坑高支模 10.AI识别 11.安全质量 智慧工地建设的价值和意义 危大工程管理 智慧工地聚焦施工现场一线生产活动&#xff0c;利用物…

sql的order by 按照自定义的顺序排列

SQL 的 ORDER BY 子句可以按照自定义的顺序进行排列。 可以使用 CASE 表达式来指定自定义的排序顺序。以下是一个示例&#xff1a; 假设我们有一个表格 students 包含字段 name 和 grade&#xff0c;我们想按照自定义的顺序对 name 字段进行排序&#xff0c;可以这样写&#…

数字孪生10个技术栈(总括):概念扫盲和总体介绍

数字孪生涉及到诸多技术领域&#xff0c;千汇数据工场将分为10个技术领域来介绍数字孪生&#xff0c;力争将复杂的技术术语给出最浅显易懂的解释&#xff0c;不会上复杂的的代码&#xff0c;大家可以畅快阅读。 本片先从总体上介绍数字孪生技术栈。 一、什么是数字孪生 数字孪…

【github】利用Git将自己的代码上传至GitHub

记录一下将自己的代码上传GitHub的步骤。    Git工具下载&#xff1a;https://git-for-windows.github.io/ 1、在github上建立项目 首先在github主页上&#xff0c;创建一个reopsitory&#xff0c;并命名为PHF-Test&#xff0c;可添加项目描述&#xff08;Description&#x…

骨传导耳机好用吗?如何挑选骨传导耳机?

骨传导耳机是一种非常创新的骨传导耳机&#xff0c;采用耳挂式佩戴&#xff0c;使用起来也非常舒适。 而且骨传导耳机最近几年还是比较火的&#xff0c;骨传导耳机的出现解决了传统入耳式耳机长时间佩戴不舒服、听力受损等问题。但随着骨传导耳机的品牌逐渐变多&#xff0c;很多…

CH32V3xx RT-Thread RS485实现modbus rtu master

目录 1、串口配置1.1 串口初始化1.2 uart DMA 初始化1.1.3 发送函数2、agile modbus3、应用测试4、遇到的问题本文通过ch32v3xx的串口 + RS485收发器实现modbus rtu master设备。此工程中移植的RT-Thread Nano系统,详情可参看本专栏前几篇文章。 1、串口配置 串口使用重映射后…

React 更改程序入口点(index.js文件位置变更)

食用前提示&#xff1a;本文基于已经快速配置好的React环境而作&#xff0c;配置React环境详见拙作&#xff1a;React环境配置-CSDN博客~ 一、了解默认入口点 使用create-react-app快速搭建react环境后&#xff0c;npm start启动程序的默认入口点为/src/index(即src目录下的ind…

如何在自定义 Angular 指令中使用 @HostBinding 和 @HostListener

简介 HostBinding 和 HostListener 是 Angular 中两个在自定义指令中非常有用的装饰器。HostBinding 允许你在承载指令的元素或组件上设置属性&#xff0c;而 HostListener 则允许你监听宿主元素或组件上的事件。 在本文中&#xff0c;你将会在一个示例指令中使用 HostBindin…

P2024 [NOI2001] 食物链 带权并查集 循环关系

题目&#xff1a; P2024 [NOI2001] 食物链 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn) 本文学习自&#xff1a; 题解 P2024 【食物链】 - RE: 从零开始的异世界信竞生活 - 洛谷博客 (luogu.com.cn) ———— 关系并查集其实就是在普通并查集的基础上额外开个数组r…

Chatgpt润色文章“咒语”

文章目录 前言一、汉译英二、语法校正三、润色英文段落结构和句子逻辑 前言 一些Chatgpt润色文章常用的命令。 一、汉译英 I am a researcher studying Aerospace Manufacturing and now trying to revise my manuscript which will be submitted to the journal of Nature.I…

数据分析 — Numpy 数组处理

目录 一、简介1、概念2、优点3、特点4、作用5、引用 二、创建数组1、创建一维数组3、创建二维数组 三、属性和数组运算1、基本属性2、数据类型3、数组运算 四、索引和切片1、基本索引2、多维数组索引3、基本切片4、多维数组切片5、布尔索引6、花式索引7、修改元素值 五、统计函…

从零开始学howtoheap:解题西湖论剑Storm_note

how2heap是由shellphish团队制作的堆利用教程,介绍了多种堆利用技术,后续系列实验我们就通过这个教程来学习。环境可参见从零开始配置pwn环境:从零开始配置pwn环境:从零开始配置pwn环境:优化pwn虚拟机配置支持libc等指令-CSDN博客 1.题目信息 https://github.com/ble55…

学习对象原型中的hasOwnProperty()

hasOwnProperty(propertyName)方法 是用来检测属性是否为对象的自有属性&#xff0c;如果是&#xff0c;返回true&#xff0c;否者false; 参数propertyName指要检测的属性名&#xff1b;

自己在开发AI应用的过程总结的 Prompt - 持续更新

自己在开发AI应用的过程总结的 Prompt - 持续更新 0. 引言1. 让模型以"中文"进行回复2. 控制模型仅输出"hi"3. 让模型"提供简单、清晰而具体的回答"4. 让模型"在最后说谢谢" 0. 引言 我想&#xff0c;我们多半有着相似的经历&#xf…

揭秘铷原子钟:北斗卫星系统的“心脏”

揭秘铷原子钟&#xff1a;北斗卫星系统的“心脏” 近日&#xff0c;中国科学院精密测量科学与技术创新研究院的梅刚华团队发布了一项重要成果。他们成功将铷原子钟的短期频率稳定度提高到了E-14&#xff08;即10的负14次方&#xff0c;相当于百万亿分之一&#xff09;的量级&a…