跳转至

2021

前言

当程序里面有使用到解析函数的时候, 静态编译程序会报warning

现象

../../lib/libhacore.a(File.cpp.o): In function `_ZN2ha4core9gid2groupB5cxx11Ej':
/tmp/src/ha/core/File.cpp:213: warning: Using 'getgrgid_r' in statically linked applications requires at runtime the shared libraries from the glibc version used for linking
../../lib/libhacore.a(File.cpp.o): In function `ha::core::group2gid(std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const&, unsigned int&)':
/tmp/src/ha/core/File.cpp:291: warning: Using 'getgrnam_r' in statically linked applications requires at runtime the shared libraries from the glibc version used for linking
../../lib/libhacore.a(File.cpp.o): In function `ha::core::username2uid(std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const&, unsigned int&)':
/tmp/src/ha/core/File.cpp:271: warning: Using 'getpwnam_r' in statically linked applications requires at runtime the shared libraries from the glibc version used for linking
../../lib/libhacore.a(File.cpp.o): In function `_ZN2ha4core12uid2usernameB5cxx11Ej':
/tmp/src/ha/core/File.cpp:194: warning: Using 'getpwuid_r' in statically linked applications requires at runtime the shared libraries from the glibc version used for linking
CMakeFiles/hasync-exec.dir/main.cpp.o: In function `boost::asio::detail::socket_ops::getaddrinfo(char const*, char const*, addrinfo const&, addrinfo**, boost::system::error_code&)':
/usr/local/include/boost/asio/detail/impl/socket_ops.ipp:3348: warning: Using 'getaddrinfo' in statically linked applications requires at runtime the shared libraries from the glibc version used for linking
/usr/local/lib/gcc/x86_64-pc-linux-gnu/8.2.0/../../../../lib64/libcrypto.a(b_sock.o): In function `BIO_gethostbyname':
b_sock.c:(.text+0x51): warning: Using 'gethostbyname' in statically linked applications requires at runtime the shared libraries from the glibc version used for linking
/usr/local/lib/gcc/x86_64-pc-linux-gnu/8.2.0/../../../../lib64/libcares.a(libcares_la-ares_getaddrinfo.o): In function `ares_getaddrinfo':
ares_getaddrinfo.c:(.text+0x73f): warning: Using 'getservbyname' in statically linked applications requires at runtime the shared libraries from the glibc version used for linking
/usr/local/lib/gcc/x86_64-pc-linux-gnu/8.2.0/../../../../lib64/libcares.a(libcares_la-ares_getnameinfo.o): In function `lookup_service.part.0.constprop.2':
ares_getnameinfo.c:(.text+0x32d): warning: Using 'getservbyport_r' in statically linked applications requires at runtime the shared libraries from the glibc version used for linking

结论

即使静态链接glibc, glibc内部在运行时依旧要调用动态库,这些库函数多与域名解析有关, 所以要求运行时的库版本要与编译时相同, 当然基于容器化部署就不用担心了

智能指针

智能指针是c++11加入的特性,包括shared_ptr和unique_ptr,weak_ptr,以及make_shared函数,但make_unique是c++14才出来
不过c++11可以通过模版来实现make_unique

普通指针和智能指针的区别

shared_ptr和unique_ptr都是异常安全的(exception-safe),而普通指针不是,举例如下

void nonsafe_call(T1* t1, T2* t2);
void safe_call(unique_ptr<T1> t1, unique_ptr<T2> t2);

nonsafe_call(new T1, new T2);
safe_call(make_unique<T1>, make_unique<T2>);

引用计数

shared_ptr.png

当形参为普通指针时

虽然new是安全的,会先申请内存再构造对象t1。 如果t1的构造函数抛异常,申请的内存会自动释放,不会内存泄漏
但当两个new作为函数参数时,情况不同。 因为参数必须在调用函数前决断,所以步骤如下

1. 为t1申请内存
2. 构造t1
3. 为t2申请内存
4. 构造t2
5. 调用函数

  • 当执行到2失败时,不会泄漏,new会释放t1的内存
  • 当执行到4失败是,会泄漏t1,因为t1已经构造完成,不会释放内存

当形参为智能指针时

步骤为

1. t1 = make_unique<T1>()
2. t2 = make_unique<T2>()
3. 调用函数
如果2失败,t1的对象由unique_ptr管理,当t1释放时,会释放内存,所以无论t1构造失败还是t2构造失败,都能正确释放内存,不会导致泄漏
就因为unique_ptr和shared_ptr是内存安全的

但以普通指针构造智能指针的方式不是异常安全的

foo(std::unique_ptr<T1>(new T1()), std::unique_ptr<T2>(new T2())); 不是异常安全的

此时步骤可能如下

1. 为t1申请内存
2. 构造t1
3. 为t2申请内存
4. 构造t2
5. 构造unique_ptr<T1>
6. 构造unique_ptr<T2>
7. 调用函数
当步骤4发生异常时,t1并未被unique_ptr管理,所以不会去释放t1的内存,故发生内存泄漏

智能指针的构造

shared_ptr对应的函数是make_shared
但c++11中没有make_unique, 可以使用模版实现如下

template<typename T,typename ...Args>
std::unique_ptr<T> make_unique(Args&& ...args){
    return std::unique_ptr<T>(new T(std::forward<Args>(args)... ));
}
那为什么这样就能异常安全呢?因为调用make_uniqe时,可以确保t1的内存被unique_ptr管理了

make_shared创建智能指针和用new创建智能指针的区别

除了上面说所的make_shared/make_unique是异常安全,而unique_ptr(new T()) 不是异常安全外,其构造的智能指针也不同

智能指针之所以会释放内存,是因为智能指针本身也是一个对象,在其生命周期结束后会调用dtor, 并在那里释放内存
所以智能指针的对象内存即包含了T,也包含了智能指针本身,而make_shared() 和 shared_ptr(new T)的区别在于前者是两个部分的地址连续,而后者并不一定连续

unique_ptr的区别

unique_ptr和shared_ptr不同,后者为了多变量同时拥有资源的访问,而unique_ptr表示任何时刻,只有一个变量能访问资源和释放资源,比如打开的文件设备, 而且通常为管理的资源设置析构函数,比如指定析构时关闭文件和设备

如下代码,当fp析构时,自动关闭文件

// helper function for the custom deleter demo below
void close_file(std::FILE* fp)
{
    std::fclose(fp);
}
using unique_file_t = std::unique_ptr<std::FILE, decltype(&close_file)>;
unique_file_t fp(std::fopen("demo.txt", "r"), &close_file);

unique_ptr要配合move使用,当move之后,原对象不拥有指针资源

总结

为了避免多参数决断时,导致已经决断的参数内存泄漏,应尽可能使用智能指针来管理内存

前言

自从知道mdns之后,发现它非常方便,例如可以找到lan下有哪些ftp服务,ssh服务等。甚至我配置好的打印机服务。

mdns介绍

有别于dns, mdns专门用来解析链路域名(TLD为*.local), 且不需要域名服务器(nameserver)。
原理是同一个lan里的主机通过avahi或zeroconfig类似的工具,向lan广播自己的ip信息和服务

avahi 介绍

avahi是zeroconfig的开源实现,在linux下还有systemd-resolve 同样可以实现mdns解析,但是avahi依旧是linux预装的软件。

avahi 配置

主要修改/etc/avahi/avahi-daemon.conf, 这里使用了br0和wlan0两个网卡,avahi就可以访问两个网络

use-ipv4=yes
allow-interfaces=br0,wlan0
再设置自启动

nss 配置

要让主机能解析到*.local 域名,需要修改配置文件/etc/nsswitch.conf

- hosts: files mymachines myhostname resolve [!UNAVAIL=return] dns
+ hosts: files mymachines myhostname mdns_minimal [NOTFOUND=return] resolve [!UNAVAIL=return] dns
这样,在主机请求解析域名的时候, 会先去请求avahi解析,如果avahi解析失败(说明不是一个local域名)才会去查找域名解析(dns)

使用systemd-resolved

对于使用systemd-resoved来管理dns解析时,可以直接用sudo systemd-resolve --set-mdns=yes --interface=eth0启动eth0端口的mdns广播。

顺便提一下,用systemd-networkd配置dns服务器和用systemd-resolved配置dns的区别,前者为每个以太网口单独配置,而systemd-resoved会读取每个以太网口配置,另外还有一个全局的dns配置。

测试

首先可以ping 本地local域名

~ > ping hst.local                                                                                                                                                                                      
PING hst.local(hst (fe80::3e9c:fff:fe8c:69c3%wlan0)) 56 data bytes
64 bytes from hst (fe80::3e9c:fff:fe8c:69c3%wlan0): icmp_seq=1 ttl=64 time=0.113 ms
64 bytes from hst (fe80::3e9c:fff:fe8c:69c3%wlan0): icmp_seq=2 ttl=64 time=0.144 ms

然后执行avahi-discover-standalone 可以查找到很多*.local的地址,特别是在公司网络里

广告自己的服务

配置好了avahi, 就可以向外界发布自己的服务,让其他人知道,也可以选择不发布
例如要广告我的ssh服务, 只需要将预设的配置文件拷贝到avahi的配置目录下

sudo cp /usr/share/doc/avahi/ssh.service /etc/avahi/services
这样,别人就能发现我的ssh服务了

配置打印机

linux的打印功能依赖CUPS这个软件,先安装CUPS,设置自启动,然后就可以访问CUPS的网页版管理界面http://localhost:631
在这个网页中添加网络打印机或者本地打印机,然后测试打印是否正常

最后,配置无线功能

上一部添加的打印机可能是有线网络打印机,也可能是串口打印机,都不能让我们的手机直接使用。

但是通过cups + avahi 就可以将你的有线打印机变成无线打印机了
首先在cups里配置,勾选Share This Printercups shares printer

然后像刚才添加ssh服务一样, 添加打印服务
/etc/avahi/services/ 创建一个service后缀的文件airprint.service

<?xml version="1.0" standalone='no'?><!--*-nxml-*-->
<!DOCTYPE service-group SYSTEM "avahi-service.dtd">
<service-group>
  <name>Printer</name>
  <service>
    <type>_ipp._tcp</type>
    <subtype>_universal._sub._ipp._tcp</subtype>
    <port>631</port>
    <txt-record>txtver=1</txt-record>
    <txt-record>qtotal=1</txt-record>
    <txt-record>rp=printers/Ricoh_MP_4055</txt-record>
    <txt-record>ty=Ricoh_MP_4055</txt-record>
    <txt-record>adminurl=http://yourip:631/printers/Ricoh_MP_4055</txt-record>
    <txt-record>note=Ricoh_MP_4055</txt-record>
    <txt-record>priority=0</txt-record>
    <txt-record>product=(GPL Ghostscript)</txt-record>
    <txt-record>printer-state=3</txt-record>
    <txt-record>printer-type=0x801046</txt-record>
    <txt-record>Transparent=T</txt-record>
    <txt-record>Binary=T</txt-record>
    <txt-record>Fax=F</txt-record>
    <txt-record>Color=T</txt-record>
    <txt-record>Duplex=T</txt-record>
    <txt-record>Staple=F</txt-record>
    <txt-record>Copies=T</txt-record>
    <txt-record>Collate=F</txt-record>
    <txt-record>Punch=F</txt-record>
    <txt-record>Bind=F</txt-record>
    <txt-record>Sort=F</txt-record>
    <txt-record>Scan=F</txt-record>
    <txt-record>pdl=application/octet-stream,application/pdf,application/postscript,image/jpeg,image/png,image/urf</txt-record>
    <txt-record>URF=W8,SRGB24,CP1,RS600</txt-record>
  </service>
</service-group>
根据情况修改一下内容,就完成了, 亲测iphone可以使用这个服务打印

注意

上面的情况假设防火墙处于关闭状态,如果启用防火墙的情况下,需要开放avahi、ssh以及ipp服务

sudo firewall-cmd --add-service ssh ipp --permanent
sudo firewall-cmd --add-port=5353/tcp --permanent
sudo firewall-cmd --reload

模板编程

模板编程是其它高级语言没有的技术, 也称为范型编程,元编程(meta programing), stl的基石。这种对类型的泛化相当于在编程之上编程

概述

模板可以作用于函数和类,即能泛化类型,也可以泛化大小

 template <typename T, int N> void foo() { T t[N]; }
 template <typename T, int N> struct Foo { T t[N]; };

 int main() {
    // array of two int
    foo<int, 2>();
    // class has a array of two int 
    Foo<int, 2> F;
  } 

模板的特化

跟泛化相反的方向,叫特化,编译器会优先使用特化的版本, 而特化有2个方向,类型特化范围特化

类型特化

对于特化类型个数的不同,分为偏特化和全特化,全特化即为所有类型都指定,特化类型越多匹配优先级越高 注意:模板函数不能偏特化

template<typename T1, typename T2> struct Foo{};
template<typename T2> struct Foo<int, T2> {};
// 全特化
template<> struct Foo<int,int>{};

例如`Foo foo',编译器会使用第三个版本

范围特化

比如常见的指针和引用,这和int, float, class 都是无关的,属于另个维度,也可以说是范围, 在stl为兼容指针做大量的工作

template <typename T, typename N> struct Foo<T *, N> {};
这样'Foo foo'会使用这个版本

还例如指定对大小的特例化

//模板
template<int n> foo(){}

//值特例化
template<> foo<10> foo(){}
那么如果调用foo<10>();时,优先匹配特例化版本

函数匹配优先级

在函数调用时,普通函数的匹配优先级高于模板函数

template <typename T> void f(T) { std::cout << "temp\n"; }
void f(int d) { std::cout << "temp1\n"; }
template <> void f(int d) { std::cout << "temp2\n"; }

f(1); // temp1

自定义类型的范围特化

上面讲的是指针类型和引用类型两种,但如果是自定义类型,那就无穷无尽了,所以模板编程也是'图灵完备'的

例如以下,创建了一个自定义的类型来包装基本类型(int,float),这样可以有自定义类型的特化版本

  template <typename T> struct Decor { using type = T; };
  template <typename T> struct Strip { using type = T; };
  template <typename T> struct Strip<Decor<T>> { using type = T; };
  template <typename T> using StripDecor = typename Strip<T>::type;

  template <typename T> class Row {};

  int main() {
    using nodecor = Row<int>;
    using decor = Decor<Row<int>>;
    // 虽底层同为int, 但nodecor 类型不同于decor类型
    static_assert(std::is_same<col>, nocol>::value);
    // 通过Strip取出其底层类型
    static_assert(std::is_same<StripDecor<col>, nocol>::value);
    return 0;
  }

模板的声明定义分离

  1. 由于template用来生成函数和类,所以编译器需要同时知道template的类型和其细节,所以模板函数不支持将定义放到源文件中
  2. 而且编译器通常是以cpp为编译单元,当编译模板cpp时不知道调用cpp, 编译调用cpp时,不知道模板cpp。所以模板函数不支持将定义放到源文件中
  3. 对于模板类可以将成员函数的定义放到源文件,但要为每个成员函数都添加'template'限定, 而且要为实例添加特例化
  4. 显式特例化支持只声明不定义,而在源文件中为每种所需的类型都特例化,即与3相同。其实显示特例化是不需要特例化而强制特例化。

如下,模板类的定义放到cpp中,这样会报错,因为在编译call_foo.cpp时不知到模板定义, 因为没有生成过int版本的Foo。
为此,必须在foo.cpp里添加template class Foo<int>, 如同4,实在吃力不讨好。

// foo.h
    template<typename T>
    class Foo {
    public:
      Foo();
      void someMethod(T x);
    private:
      T x;
    };

// foo.cpp
    template<typename T>
    Foo<T>::Foo()
    {
      // ...
    }
    template<typename T>
    void Foo<T>::someMethod(T x)
    {
      // ...
    }

// call_foo.cpp
    void blah_blah_blah()
    {
      // ...
      Foo<int> f;
      f.someMethod(5);
      // ...
    }

当然把模板的定义放到头文件中会增加可执行文件的体积。

实际运用

grpc 能以流和非流方式传输,而grpc参数protobuf消息类型是另一个范围。那么要封装grpc方法, 需要封装流+类型

举例protobuf的消息类型有string, fixed32 - string - fixed32 - Stream - Stream

通过上面的偏特化可以即能区分流和非流又能区分类型

赘述一下模板的类型

上面的模板类型T都是实际编程时定义的类型,但作为图灵完备的模板编程,未决的template类型也可以作为template类型

如下是一个模板用另一个模板来特例化

template <typename T> struct Upper {};
template <template <typename> class T> struct Lower {};

template<typename T>
Lower<Upper<T>> l;

可变模版variadic templates

c中有可变参数...和gcc内置__VA_ARGS__宏定义, 实现不同个数的变量打印。这是由编译期实现的,会将format的格式符替换成参数

int printf ( const char * format, ... );

c++有模版,而且在c++11之后引入了动态参数模版,即模版函数或类可以使用动态参数

实际运用1

在实际项目中手动跑单元测试用例的时候,不希望再去看日志文件,而是想日志直接输出到终端, 有以下办法

c++11及以上标准

可以使用动态参数模版替换原本的日志打印函数

#undef log_debug

template<typename First, typename ...Rest>
void log_debug(First && first, Rest && ... rest){
  std::cout << fmt::format(first, rest ...) << std::endl;
}

// 这时日志就直接输出到终端了, 这里使用了fmt库
log_debug("aasdas{}", "bbbb");

也可简单写成

template<typename ...Args>
void log_debug(Args&& ...args){
    std::cout << fmt::format(args...) << std::endl;
}

如果不使用fmt格式化,还可以用

#undef log_debug
// 定义一个空函数
void log_debug(){}

template<typename First, typename Rest>
void log_debug(First&& first, Arg&& ...arg){
    std::cout << first;
    log_debug(arg...);
}
需要解释一下,函数log_debug()必须要先声明,因为模版实例化的的最终要调用这个无参的函数
模拟一下堆栈, 因为参数在每次递归时减少一个,所以最终是0个参数
log_debug(1, 0.2, "aaa");
log_debug(0.2, "aaa");
log_debug("aaa");
log_debug();

c++11以前的标准

可以使用宏定义来替换了, 然后需要重载,方法

#define log_debug(...) std::cout , __VA_ARGS__ , std::endl

template <typename T>
std::ostream& operator,(std::ostream& out, const T& t) {
  out << t;
  return out;
}

//overloaded version to handle all those special std::endl and others...
std::ostream& operator,(std::ostream& out, std::ostream&(*f)(std::ostream&)) {
  out << f;
  return out;
}

直接用c的方式

因为printf是支持varidic的

#undef log_debug

#define log_debug(...) printf(__VA_ARGS__), printf("\n")

int main() {
  log_debug("example","output","filler","text");
  return 0;
}

c++17 引入了fold expression

可以改写为

template<typename ...Args>
void log_debug(Args && ...args)
{
    (std::cout << ... << args);
}

动态参数模版除了以上的用法,还有更多用处, 例如std::tupe的实现

实际运用2

使用模板生成并发代码

以下代码实现复制二维数组,

for( size_t ch=0 ; ch<channelNum ; ++ch ) {
    for( size_t i=0; i<length ; ++i ) {
        out[ch][i]=in[ch][i];
    }
}

但以下理论更快,没有两层for,前提是知道channel大小

for(size_t i=0;i<length;++i) {
    out[0][i]=in[0][i];
    out[1][i]=in[1][i];
    out[2][i]=in[2][i];
    out[3][i]=in[3][i];
}

但如果用模板,就不需要知道channel大小,自动生成上面的代码

template <int count> class Copy {
public:
  static inline void go(float **const out, float **const in, int i) {
    Copy<count - 1>::go(out, in, i);
    out[count - 1][i] = in[count - 1][i];
  }
};

template <> class Copy<0> {
public:
  static inline void go(float **const, float **const, int) {}
};

template <int channelNum>
void parall_copy(float **out, float **in, size_t length) {
  for (size_t i = 0; i < length; ++i) {
    Copy<channelNum>::go(out, in, i);
  }
}

需要提醒的,如同打印日志的,0的特例化不能省,否则编译出错

实际运用3 工厂模式

在使用spdlog时发现有使用到template未决名, 用来实现两个维度的工厂模式。

第一层提供两种sink的工厂,而其factory是未决名,所以要加上Factory::template消歧义,不然<会当成小于号

//
// factory functions
//
template<typename Factory = default_factory>
inline std::shared_ptr<logger> basic_logger_mt(const std::string &logger_name, const filename_t &filename, bool truncate = false)
{
    return Factory::template create<sinks::basic_file_sink_mt>(logger_name, filename, truncate);
}

template<typename Factory = default_factory>
inline std::shared_ptr<logger> basic_logger_st(const std::string &logger_name, const filename_t &filename, bool truncate = false)
{
    return Factory::template create<sinks::basic_file_sink_st>(logger_name, filename, truncate);
}

此时factory可以是synchronous_factory, 也可以是异步版本,但这需要用户自己实现。

// Default logger factory-  creates synchronous loggers
struct synchronous_factory
{
    template<typename Sink, typename... SinkArgs>
    static std::shared_ptr<spdlog::logger> create(std::string logger_name, SinkArgs &&... args)
    {
        auto sink = std::make_shared<Sink>(std::forward<SinkArgs>(args)...);
        auto new_logger = std::make_shared<logger>(std::move(logger_name), std::move(sink));
        details::registry::instance().initialize_logger(new_logger);
        return new_logger;
    }
};

using default_factory = synchronous_factory;

简化为下面的demo,固然可以直接使用call_dd的方式,但维度只有一个。而call_dd2则有2个维度了
但必须使用T::template消歧义, 因为此时的foo未决名, 不知道是那个类里面的foo。

  struct AA {
    template <typename cc> static void foo() { std::cout << "dd::foo\n"; };
  };

  struct BB {
    template <typename cc> static void foo() { std::cout << "dd::foo\n"; };
  };

  template <typename T> void call_dd() { AA::foo<T>(); }
  template <typename T, typename K> void call_dd2() { T::template foo<K>(); }

  int main() {
    call_dd<void>();
    call_dd2<AA>();
    call_dd2<AA, int>();
    call_dd2<BB, long>();
  }

模版与宏定义、虚函数的区别

  1. 宏定义在预处理期执行,模板在编译期执行,而虚函数也称动态绑定在运行时执行
  2. 宏和模板都将运行时的工作提前了,用编译时间换取运行效率
  3. 宏定义没有类型检查,这点模板比较好
  4. 模板虽然会延长编译时间,但当编译期实例化类型后,查找模板函数和查找普通函数的速度几乎相同

待决名dependent name

  1. 待决名的意思是在定义的地方,类型还不能决断,需要延后到实例化确定时。而非待决名指类型在定义的地方已经确定。
  2. 延后将导致此时无法在定义点进行错误检查,以及消除typenametemplate歧义,这导致需要在调用点加上template

待决名如:

template<typename T>
struct X : B<T> // "B<T>" 取决于 T
{
    typename T::A* pa; // "T::A" 取决于 T
                       // (此 "typename" 的使用的目的见下文)
    void f(B<T>* pb)
    {
        static int i = B<T>::i; // "B<T>::i" 取决于 T
        pb->j++; // "pb->j" 取决于 T
    }
};

让人吃惊的例子, 这就是非待决名的情况下,立即绑定

#include <iostream>

void g(double) { std::cout << "g(double)\n"; }

template<class T>
struct S
{
    void f() const
    {
        g(1); // "g" 是非待决名,现在绑定
    }
};

void g(int) { std::cout << "g(int)\n"; }

int main()
{
    g(1);  // 调用 g(int)

    S<int> s;
    s.f(); // 调用 g(double)
}

typename消歧义

在模板(包括别名模版)的声明或定义中,不是当前实例化的成员且取决于某个模板形参的名字不会被认为是类型,
除非使用关键词 typename 或它已经被设立为类型名(例如用 typedef 声明或通过用作基类名)。

#include <iostream>
#include <vector>

int p = 1;

template<typename T>
void foo(const std::vector<T> &v)
{
    // std::vector<T>::const_iterator 是待决名,
    typename std::vector<T>::const_iterator it = v.begin();

    // 下列内容因为没有 'typename' 而会被解析成
    // 类型待决的成员变量 'const_iterator' 和某变量 'p' 的乘法。
    // 因为在此处有一个可见的全局 'p',所以此模板定义能编译。
    std::vector<T>::const_iterator* p; 

    typedef typename std::vector<T>::const_iterator iter_t;
    iter_t * p2; // iter_t 是待决名,但已知它是类型名
}

template<typename T>
struct S
{
    typedef int value_t; // 当前实例化的成员
    void f()
    {
        S<T>::value_t n{}; // S<T> 待决,但不需要 'typename'
        std::cout << n << '\n';
    }
};

int main()
{
    std::vector<int> v;
    foo(v); // 模板实例化失败:类型 std::vector<int> 中没有
            // 名字是 'const_iterator' 的成员变量
    S<int>().f();
}

template消歧义

与此相似,模板定义中不是当前实例化的成员的待决名同样不被认为是模板名,除非使用消歧义关键词 template,或它已被设立为模板名:

template<typename T>
struct S
{
    template<typename U> void foo() {}
};

template<typename T>
void bar()
{
    S<T> s;
    s.foo<T>();          // 错误:< 被解析为小于运算符
    s.template foo<T>(); // OK
}
template 消歧义可以使用
T::template
s.template
this->template

std::forward 转发在模版的使用

为什么完美转发的对象必须是右值引用?

说明一下右值引用

 引用类型   可以引用的值类型    使用场景
非常量左值   常量左值    非常量右值   常量右值
非常量左值引用     Y   N   N   N   无
常量左值引用  Y   Y   Y   Y   常用于类中构建拷贝构造函数
非常量右值引用     N   N   Y   N   移动语义、完美转发
常量右值引用  N   N   Y   Y   无实际用途

参考

https://en.cppreference.com/w/cpp/language/parameter_pack https://en.cppreference.com/w/cpp/language/fold https://en.cppreference.com/w/cpp/language/overload_resolution#Best_viable_function

临时变量

我们说的临时变量通常是指在语句块里定义的短暂生命周期的局部变量, 其存储周期为'auto',但这里讨论的是c++中'temporary object', 是在老版本c++中 而语句块里定义的变量以及非引用类型参数,并不属于这里的临时变量

c++中的临时变量出现情况: * litteral常量, 如1 * 类型转换 // 赋值语句结束后,自动销毁 * 函数返回值 // 赋值语句结束后,自动销毁 * 表达式的值

而临时变量的定义就是编译器自动创建和销毁的没有名字的变量, 也无法取地址。并且规定:
- 常量类型的引用(reference to const)可以绑定到临时变量 - 非常量类型的引用(reference to non-const)类型不能绑定到临时变量

这个规定的原因是: 因为上面说的编译器自动创建和销毁,所以去修改一个会销毁变量是没有意义的

什么时候编译器要需要创建临时变量

创建原因 销毁时机
Result of expression evaluation All temporaries created as a result of expression evaluation are destroyed at the end of the expression statement (that is, at the semicolon), or at the end of the controlling expressions for for, if, while, do, and switch statements.
Initializing const references If an initializer is not an l-value of the same type as the reference being initialized, a temporary of the underlying object type is created and initialized with the initialization expression. This temporary object is destroyed immediately after the reference object to which it is bound is destroyed.

主要是以下两个地方会需要临时变量: * 函数接受引用类型参数的时候 * 函数返回的时候 * 类型转换的时候

举例:

函数参数

void foo(const int &arg){}
foo(1);  // 此时创建了临时变量,可以想象成'foo(int _tmp(i));' 而且这个_tmp 不能被修改

函数返回值

int foo(){return 1;}
int i=foo(); // 此时创建了临时变量, int _tmp = foo(); int i = _tmp;

类型转换

int i = 1;
double d = i; // double _tmp = i; double d = _tmp;

单独讨论这些似乎意义不大, 但是在实际编写代码的时候,可能会有疑惑

例如如何解释

const int& cr = 1; // ok
int &r = 1; // ng
这里赋值和函数传参一样, 因为1是常数,这里为常量1创建临时变量, 而只有reference to const 才能绑定到临时变量上

还有解释一个经常被引用的例子

void foo(int &arg){}
int i = 1; foo(i); // ok
double d = 1.0; foo(d); // ng

void foo(const int &arg){}
int i = 1; foo(i); // ok
double d = 1.0; foo(d); // ok
这里虽然i和d都不是常量(不会创建临时变量), 但是因为类型不同, 发生了类型转换,创建了临时变量。同样地,只有reference to const 才能绑定到临时变量上

kmp 子串查找算法

leetcode的题目, 实现strstr,即查找子字符串

最简单的算法

int strStr(char * haystack, char * needle){
    char *tmp = haystack;
    while(*tmp != 0){
        if(strncmp(tmp, needle,strlen(needle))==0){
            return tmp - haystack;
        }
        tmp ++;
    }
    return -1;
}

kmp 算法

// C++ program for implementation of KMP pattern searching 
// algorithm 
#include <bits/stdc++.h> 

void computeLPSArray(char* pat, int M, int* lps); 

// Prints occurrences of txt[] in pat[] 
void KMPSearch(char* pat, char* txt) 
{ 
    int M = strlen(pat); 
    int N = strlen(txt); 

    // create lps[] that will hold the longest prefix suffix 
    // values for pattern 
    int lps[M]; 

    // Preprocess the pattern (calculate lps[] array) 
    computeLPSArray(pat, M, lps); 

    int i = 0; // index for txt[] 
    int j = 0; // index for pat[] 
    while (i < N) { 
        if (pat[j] == txt[i]) { 
            j++; 
            i++; 
        } 

        if (j == M) { 
            printf("Found pattern at index %d ", i - j); 
            j = lps[j - 1]; 
        } 

        // mismatch after j matches 
        else if (i < N && pat[j] != txt[i]) { 
            // Do not match lps[0..lps[j-1]] characters, 
            // they will match anyway 
            if (j != 0) 
                j = lps[j - 1]; 
            else
                i = i + 1; 
        } 
    } 
} 

// Fills lps[] for given patttern pat[0..M-1] 
void computeLPSArray(char* pat, int M, int* lps) 
{ 
    // length of the previous longest prefix suffix 
    int len = 0; 

    lps[0] = 0; // lps[0] is always 0 

    // the loop calculates lps[i] for i = 1 to M-1 
    int i = 1; 
    while (i < M) { 
        if (pat[i] == pat[len]) { 
            len++; 
            lps[i] = len; 
            i++; 
        } 
        else // (pat[i] != pat[len]) 
        { 
            // This is tricky. Consider the example. 
            // AAACAAAA and i = 7. The idea is similar 
            // to search step. 
            if (len != 0) { 
                len = lps[len - 1]; 

                // Also, note that we do not increment 
                // i here 
            } 
            else // if (len == 0) 
            { 
                lps[i] = 0; 
                i++; 
            } 
        } 
    } 
} 

// Driver program to test above function 
int main() 
{ 
    char txt[] = "ABABDABACDABABCABAB"; 
    char pat[] = "ABABCABAB"; 
    KMPSearch(pat, txt); 
    return 0; 
} 

前言

说一个老话, 现在systemd作为linux的启动管理和服务管理已经越来越重要了, 上周考试也遇到用systemd 来管理容器,这里记录一下如何编写systemd服务

关于systemd

systemd是只能运行在Linux上的init, 也就是启动后看到的1号进程。 除了启动, systemd还管理着很多东西,例如网络(systemd-networkd), 域名解析(systemd-resolved),为服务创建socket(systemd.socket) 文件系统挂载,还有系统和用户的服务
systemd太大,说不完,需要查看各种文档

systemd 的两种使用模式

systemd 分为system级别和user级别, 对应的unit文件分别在/etc/systemd/ 和 ~/.config/systemd/下, 前者是系统级别,后者是用户级别。 用户只能运行自己设置的服务

systemctl start system_service.service
而普通用户只能执行
systemctl --user user_service.service

这个name就是文件名称,例如必须'/etc/systemd/system/'下存在'system_service.service'文件,在能执行第一条的命令、 必须在 '~/.config/systemd/user/'下存在'user_service.service'在能执行第二条命令

系统服务以其他用户运行服务

系统级别的服务默认会以root来运行服务,但是也可以设置以其他用户来运行来最小化权限,例如音视频服务。也可以以某个用户来执行,那么service unit 文件就变为'system_service@user.service'

# system_service@user.service
[Unit]
Description=Watchman for user %i
After=remote-fs.target
Conflicts=shutdown.target

[Service]
ExecStart=/usr/local/bin/watchman --foreground --inetd
ExecStop=pkill -u %i -x watchman
Restart=on-failure
User=%i
Group=users
StandardInput=socket
StandardOutput=syslog
SyslogIdentifier=watchman-%i

[Install]
WantedBy=multi-user.target

上面的服务以下面的socket 单元启动, 前提要这个服务实现接收socket,通过sd_listen_fds(3)

# system_service@user.socket

[Unit]
Description=Watchman socket for user %i

[Socket]
ListenStream=/var/facebook/watchman/%i-state/sock
Accept=false
SocketMode=0664
SocketUser=%i
SocketGroup=othergroup

[Install]
WantedBy=sockets.target

普通用户运行服务

注意, 普通用户因为只会以自己的身份启动,所以不能想系统服务那样指定'User/Group'

[Unit]
Description=tun2socks for vpn
#Requires=ssh_to_alpha.service

[Service]
Type=simple
ExecStart=/usr/bin/badvpn-tun2socks 

[Install]
WantedBy=default.target

若希望这个用户自定义服务能自启动, WantedBy需要设置成'default.target'

自动创建unit-file

以下命令可以自动在对应目录创建*.service文件

systemctl --user --force --full edit test.service 

rhel系列修改密码

考rhce8 栽在改密码了, 现在彻底弄明白

关于selinux

这是rhel和其他发行版的最大区别,也是我忽略的点。启用selinux 时,改密码后,额外要执行touch /.autorelabel, 新密码才能生效,而平时我使用centos一直是禁用selinux的。

启用selinux

selinux 默认状态是enforcing, 禁用时为disable, 通过sestate 查看状态。

编译'/etc/selinux/config'

修改selinux配置,从disable 到enforcing

执行两次下面动作

创建这个文件的意义是重新label selinux, 不仅是当修改selinux配置需要做,在重置root密码时也是需要

touch /.autorelabel
reboot

叙说改密

rhel5~8有两种方式重置密码,老版本为给linux启动参数加上'init=/bin/sh', 新版本为加rd.break
老版本适用于centos8(已测), 而新版本应该不支持rhel5,6(未测),下面是完整步骤

老版本

  1. 进入grub界面,按e进入编辑启动参数,到linux 行,按ctrl+e或者end键到末尾,追加'init=/bin/sh'
  2. 按ctrl+x继续,系统自动进入内存文件系统的根目录
  3. 执行/sbin/load_policy -i来初始化selinux
  4. 此时系统处于ro模式,执行mount -oremount,rw / 重新挂载根分区,使系统可写
  5. passwd 设置root密码
  6. 如果启用了selinux, 额外要执行touch /.autorelabel
  7. 最后执行exit或者 exec /sbin/reboot 或者exec /sbin/init

新版本

  1. 进入grub界面,按e进入编辑启动参数,到linux 行,按ctrl+e或者end键到末尾,追加'rd.break'
  2. 按ctrl+x继续,系统自动进入root系统,此时真正的文件系统以ro挂载在/sysroot
  3. 执行mount -oremount,rw /sysrot 重新挂载
  4. 执行chroot /sysroot 进入系统
  5. 执行passwd设置root密码
  6. 如果启用了selinux, 需要额外创建文件touch /.autorelabel
  7. 执行exit 退出,然后执行 umount /sysroot 来确保写入
  8. reboot 来重新进入

通过rescue模式改密码

任何发行版都可以通过光盘引导来改密。如果熟悉archlinux安装的方式,就知道进入live os之后可以挂载原系统的磁盘
然后chroot成为root用户,就能直接执行passwd来修改密码

通过修改镜像改密码

最暴力的方式就是用guestfish工具, 一条命令改密。当然得先获取镜像文件
先切换到root用户,使用guestmount命令挂载分区,-i表示自动挂载

guestmount --add base.raw  -i /tmp/hm
然后chroot到挂载点
chroot /tmp/hm
然后改密
passwd

virt提供了简化版本,一条命令就能改密码

virt-customize -a centos8.qcow2 --root-password password:123456

可见镜像文件有多么不安全

最后记录一下vmware虚拟机镜像转换kvm镜像

由于收到的vmware镜像,又没有装vmware workstation,所以找到转换镜像的方法,也很方便

qemu-img convert  CentOS\ 5.vmdk  base-000001.raw       
但问题是不支持转换快照文件

引用

https://docs.openstack.org/image-guide/convert-images.html

weechat 使用方法

weechat 是一个irc客户端, 在终端中运行,不需要gui桌面,非常方便。 这里记录配置方法

主要参考链接

安装和运行weechat

安装后,直接在终端运行weechat,就能进入weechat

配置

添加server

irc有很多server, 常用的是freenode。 另外有darkscience。 添加方法如下,另外weechat里面命令都是以'/' 开头,还能使用tab补全

/server add freenode chat.freenode.net/6697 -ssl
irc 除了有很多服务器url, 每个url 还有多个端口供链接,比如6697

设置昵称

昵称默认是linux系统的用户名,昵称用来在聊天中显示名称

/nick mynickname
**注意: 手动修改昵称不是简单的事情,特别是当已经连接serer时。 比较方便的办法就是直接改配置文件
# ~/.weechat/irc.conf
nicks = "malloy,malloy1,malloy2"
带后缀的昵称用来当malloy占用时的备用,比如网络重连时昵称有冲突就需要备用昵称

注册昵称

因为有很多聊天室要求注册后才能进入,所以先要使用邮箱注册

/msg nickserv register userpassword example@email

然后irc 会提示查收邮件,在邮件里面提示你在irc输入命令完成注册

加入聊天室

聊天室都是以#开头,例如我使用的 #archlinuxcn

/join #weechat

退出聊天室

/parted "leave message"
或者
/close

自动设置

设置打开weechat时,自动登陆和自动进入聊天室

/set irc.server.freenode.autoconnect on
/set irc.server.networkname.autojoin "#channel1,#channel2"

界面设置

先介绍weechat的各名称对应命令 : 聊天室称为buffer, 切换聊天室命令 /buffer n
最左边的聊天列表是bufferlist bar
进入buffer后,最右边的是nicklist bar,一个聊天室界面叫做window, 所以下面的命令设置就与 /bar /windows 有关

隐藏和显示bufferlist

/bar hiden bufferlist 
/bar show bufferlist
/bar toggle bufferlist
分屏显示所有聊天室,水平分屏splith,竖直分屏 splitev
/window splitv 
窗口切换
/window +1
/window -1
窗口合并
/window merge

安全设置

  1. 上面添加服务器时,已经配置了ssl选项,就表示使用通讯加密了,但irc的聊天记录一般是对外公开的
  2. 设置weechat开启密码, 进入weechat前验密
    /secure passphrase superSecretPassphrase
    

自动化设置和鉴权

注册好昵称之后,配置自动化登陆,而不需要手动执行命令

设置irc登陆昵称,作为identify的参数,务必要跟register的保持一致

/secure set networkname_nickname password
设置irc登陆密码,作为identify的参数,务必要跟register的密码保持一致
/secure set networkname_password password

设置weechat启动时自动登陆服务器,以及自动加入channel

/set irc.server.libera.autoconnect on
/set irc.server.libera.autojoin "#archlinux-cn,#c,#c++"
以及自动验密
/set irc.server.networkname.command "/msg nickserv identify ${sec.data.networkname_nickname} ${sec.data.network_password}"
其实显而易见地,sec.data.networkname_nicknamesec.data.network_password 都只是保存在sec.data里的变量,其实可以两个变量合成一条也没问题

更多功能

weechat有很多拓展插件,可以完成很多事情,比如自动回复,远程控制,连接telegram等, 参考

udp 的端口复用实现负载均衡

前言

偶尔看到 python 3.9 的release note 里面提到一个bug

asyncio¶

出于重要的安全性考量,asyncio.loop.create_datagram_endpoint() 的 reuse_address 形参不再被支持。 这是由 UDP 中的套接字选项 SO_REUSEADDR 的行为导致的。 更多细节请参阅 loop.create_datagram_endpoint() 的文档。 (由 Kyle Stanley, Antoine Pitrou 和 Yury Selivanov 在 bpo-37228 中贡献。。)
意思是tcp的socket option:SO_REUSEADDR不适用于udp: 在tcp中这个选项表示立即回收端口,减少 time_wait 的时间。而在udp中,这个选项表示多个socket可以绑定一个端口, 由内核来分发请求。

所以看到此功能,自己试了一下,确实如此, 顺便回顾一下知识

主要代码

    struct sockaddr_in addr;
    addr.sin_family = AF_INET;
    addr.sin_port = htons(8888);
    inet_pton(AF_INET,"127.0.0.1",(void*)&addr.sin_addr);
    // inet_pton 支持ipv4和ipv6,是比较新的转换函数

    sfd = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
    if(sfd == -1)
    {
        perror("socket");
        exit(1);
    }
    int val = 1;
    if(0 != setsockopt(sfd, SOL_SOCKET, SO_REUSEPORT,&val, sizeof(val))){
        perror("setsockopt");
        exit(1);
    }
    /* sockaddr 和 sockaddr_in 有什么区别?
       struct sockaddr {  
        sa_family_t sin_family;//地址族
        char sa_data[14]; //14字节,包含套接字中的目标地址和端口信息               
      }; 
      struct sockaddr_in {
        sa_family_t sin_family;//地址族
        uint16_t sin_port;
        struct in_addr sin_addr;    // 32 位地址
        char    sin_zero[8];    // reserve
      };
      struct in_addr {
        In_addr_t   s_addr;  //32位
      };
      
      sockaddr_in 和 sockaddr 长度相同,都 sin_family + 14 个字节,但是前者显式划分了
   */ 
    if(bind(sfd, (struct  sockaddr*) &addr, sizeof(addr)) != 0)
    {
        perror("bind");
        exit(1);
    }

效果

先启动两个服务端,再使用ncat 来模拟请求,

ncat -uv 0.0.0.0 8888

启动ncat时,系统会分配给一个服务端处理。 但是重启ncat时, 会切换到另一个服务端处理