引子

凡是涉及STL的错误都不堪入目,因为首先STL中有复杂的层次关系,在错误信息中都会暴露出来,其次这么多类和函数的名字大多都是双下划线开头的,一般人看得不习惯。

一个经典的错误是给std::sort传入std::list<T>的迭代器:

#include <list>
#include <algorithm> int main()
{
std::list<int> list;
std::sort(list.begin(), list.end());
}

GCC 10.1.0给出如下错误信息(没有开-std=c++20):

In file included from c:\program files\mingw-w64\include\c++\10.1.0\algorithm:62,
from temp.cpp:3:
c:\program files\mingw-w64\include\c++\10.1.0\bits\stl_algo.h: In instantiation of 'void std::__sort(_RandomAccessIterator, _RandomAccessIterator, _Compare) [with _RandomAccessIterator = std::_List_iterator<int>; _Compare = __gnu_cxx::__ops::_Iter_less_iter]':
c:\program files\mingw-w64\include\c++\10.1.0\bits\stl_algo.h:4859:18: required from 'void std::sort(_RAIter, _RAIter) [with _RAIter = std::_List_iterator<int>]'
temp.cpp:9:39: required from here
c:\program files\mingw-w64\include\c++\10.1.0\bits\stl_algo.h:1975:22: error: no match for 'operator-' (operand types are 'std::_List_iterator<int>' and 'std::_List_iterator<int>')
1975 | std::__lg(__last - __first) * 2,
| ~~~~~~~^~~~~~~~~
In file included from c:\program files\mingw-w64\include\c++\10.1.0\bits\stl_algobase.h:67,
from c:\program files\mingw-w64\include\c++\10.1.0\bits\char_traits.h:39,
from c:\program files\mingw-w64\include\c++\10.1.0\ios:40,
from c:\program files\mingw-w64\include\c++\10.1.0\ostream:38,
from c:\program files\mingw-w64\include\c++\10.1.0\iostream:39,
from temp.cpp:1:
c:\program files\mingw-w64\include\c++\10.1.0\bits\stl_iterator.h:500:5: note: candidate: 'template<class _IteratorL, class _IteratorR> constexpr decltype ((__y.base() - __x.base())) std::operator-(const std::reverse_iterator<_Iterator>&, const std::reverse_iterator<_IteratorR>&)'
500 | operator-(const reverse_iterator<_IteratorL>& __x,
| ^~~~~~~~
c:\program files\mingw-w64\include\c++\10.1.0\bits\stl_iterator.h:500:5: note: template argument deduction/substitution failed:
In file included from c:\program files\mingw-w64\include\c++\10.1.0\algorithm:62,
from temp.cpp:3:
c:\program files\mingw-w64\include\c++\10.1.0\bits\stl_algo.h:1975:22: note: 'std::_List_iterator<int>' is not derived from 'const std::reverse_iterator<_Iterator>'
1975 | std::__lg(__last - __first) * 2,
| ~~~~~~~^~~~~~~~~
In file included from c:\program files\mingw-w64\include\c++\10.1.0\bits\stl_algobase.h:67,
from c:\program files\mingw-w64\include\c++\10.1.0\bits\char_traits.h:39,
from c:\program files\mingw-w64\include\c++\10.1.0\ios:40,
from c:\program files\mingw-w64\include\c++\10.1.0\ostream:38,
from c:\program files\mingw-w64\include\c++\10.1.0\iostream:39,
from temp.cpp:1:
c:\program files\mingw-w64\include\c++\10.1.0\bits\stl_iterator.h:1533:5: note: candidate: 'template<class _IteratorL, class _IteratorR> constexpr decltype ((__x.base() - __y.base())) std::operator-(const std::move_iterator<_IteratorL>&, const std::move_iterator<_IteratorR>&)'
1533 | operator-(const move_iterator<_IteratorL>& __x,
| ^~~~~~~~
c:\program files\mingw-w64\include\c++\10.1.0\bits\stl_iterator.h:1533:5: note: template argument deduction/substitution failed:
In file included from c:\program files\mingw-w64\include\c++\10.1.0\algorithm:62,
from temp.cpp:3:
c:\program files\mingw-w64\include\c++\10.1.0\bits\stl_algo.h:1975:22: note: 'std::_List_iterator<int>' is not derived from 'const std::move_iterator<_IteratorL>'
1975 | std::__lg(__last - __first) * 2,
| ~~~~~~~^~~~~~~~~

太长不看,加三告辞。换个Visual Studio 2019:

Severity	Code	Description	Project	File	Line	Suppression State
Error C2676 binary '-': 'const std::_List_unchecked_iterator<std::_List_val<std::_List_simple_types<_Ty>>>' does not define this operator or a conversion to a type acceptable to the predefined operator temp C:\Program Files (x86)\Microsoft Visual Studio\2019\Community\VC\Tools\MSVC\14.27.29110\include\algorithm 4138
Error C2672 '_Sort_unchecked': no matching overloaded function found temp C:\Program Files (x86)\Microsoft Visual Studio\2019\Community\VC\Tools\MSVC\14.27.29110\include\algorithm 4138
Error C2780 'void std::_Sort_unchecked(_RanIt,_RanIt,iterator_traits<_Iter>::difference_type,_Pr)': expects 4 arguments - 3 provided temp C:\Program Files (x86)\Microsoft Visual Studio\2019\Community\VC\Tools\MSVC\14.27.29110\include\algorithm 4138

虽然错误信息简短许多,但仍不能告诉我们错误的原因(这些是内部原因)。

我们注意到两段错误都提到了operator-,实际上编译器认为错误在于std::sort中会把两个输入迭代器所属类型的实例相减,而std::list<T>::iterator没有重载operator-运算符。这当然不是让我们来重载这个运算符。

STL源码可以提供一些帮助:

  /**
* @brief Sort the elements of a sequence.
* @ingroup sorting_algorithms
* @param __first An iterator.
* @param __last Another iterator.
* @return Nothing.
*
* Sorts the elements in the range @p [__first,__last) in ascending order,
* such that for each iterator @e i in the range @p [__first,__last-1),
* *(i+1)<*i is false.
*
* The relative ordering of equivalent elements is not preserved, use
* @p stable_sort() if this is needed.
*/
template<typename _RandomAccessIterator>
_GLIBCXX20_CONSTEXPR
inline void
sort(_RandomAccessIterator __first, _RandomAccessIterator __last)
{
// concept requirements
__glibcxx_function_requires(_Mutable_RandomAccessIteratorConcept<
_RandomAccessIterator>)
__glibcxx_function_requires(_LessThanComparableConcept<
typename iterator_traits<_RandomAccessIterator>::value_type>)
__glibcxx_requires_valid_range(__first, __last);
__glibcxx_requires_irreflexive(__first, __last); std::__sort(__first, __last, __gnu_cxx::__ops::__iter_less_iter());
}

在概念上(conceptually),std::list<T>的迭代器不满足RandomAccessIterator的要求,所以不能用于std::sort。然而_RandomAccessIterator毕竟只是一个名字,编译器不知道它表示哪些要求,更无法据此输出错误信息。

但是从C++20开始,编译器可以掌握这些信息了,不是通过typename后面的那个名字,而是由两个新关键词conceptrequires支撑起来的。然后对于上面那个错误,编译器会说:“std::random_access_iterator<std::list<int>::iterator>不成立”(尽管目前我还没有体验过这种编译器)。

如果我们自己写的模板函数对类型有要求,可以在模板参数列表中写出:

#include <iterator>

template<std::random_access_iterator Iter>
void func(Iter _first, Iter _last)
{
// ...
}

那么std::random_access_iterator是如何实现的呢?

template<typename _Iter>
concept random_access_iterator = bidirectional_iterator<_Iter>
&& derived_from<__detail::__iter_concept<_Iter>,
random_access_iterator_tag>
&& totally_ordered<_Iter> && sized_sentinel_for<_Iter, _Iter>
&& requires(_Iter __i, const _Iter __j,
const iter_difference_t<_Iter> __n)
{
{ __i += __n } -> same_as<_Iter&>;
{ __j + __n } -> same_as<_Iter>;
{ __n + __j } -> same_as<_Iter>;
{ __i -= __n } -> same_as<_Iter&>;
{ __j - __n } -> same_as<_Iter>;
{ __j[__n] } -> same_as<iter_reference_t<_Iter>>;
};

意思看得懂,但不会写。别着急,这些语法我们一点点来讲。

requires关键词与需求

对模板参数的需求是嵌套的,深入到最底层,都是通过requires关键词实现的。“s”的存在使代码在英语的语法中更加通顺一点。

requires有两种用法:requires子句(requires-clause)和requires表达式。

requires表达式

requires表达式产生一个bool值,语法为下列之一:

  • requires { 一系列requirements(需求) }

  • requires ( 参数列表 ) { 一系列requirements }

参数列表用于创建一系列一定类型的变量,在requirements中使用。这些变量并不真实存在(只有语法功能),它们的作用域到后面的}为止。

Requirements有四种:简单需求(simple requirements)、类型需求(type requirements)、复合需求(compound requirements)和嵌套需求(nested requirements)。Requirements之间由分号分隔,只有当每个都满足时整个表达式才为true

我们后面再来看requires表达式怎么用,现在我们要了解的是我们可以提出哪些需求。

简单需求

任意不以requires关键词开头的表达式都可以作为简单需求,当该表达式语法正确时需求满足。由于参数列表中的变量不实际存在,这个表达式当然也不会被求值。

requires (T a, T b)
{
a + b;
}

类型需求

typename后跟一个类型名成为类型需求,当该类型存在时需求满足。类型需求可以用来检查嵌套类型和模板实例化。

requires
{
typename T::type;
typename S<T>;
}

复合需求

复合需求要求一个表达式合法,且结果类型符合一定约束,并可规定noexcept

{ 表达式 } 可选的noexcept -> concept名 可选的<参数列表>;

后面会讲类型代入concept的规则,毕竟现在连concept都没讲呢。

requires (T x)
{
{++x} -> std::same_as<T&>;
}

嵌套需求与requires子句

嵌套需求就是requires子句(这句话不太严格,但没有必要纠结它们的区别)。requires后跟一个bool常量成为一个requires子句,仅当该bool常量的值为true时,子句所在的需求被满足,或所在的模板有效。预告一下,把参数代入一个concept可以得到truefalse,而一个concept可以包含多个需求,所以嵌套需求就是多条已定义的需求的组合。

requires (T x) // requires表达式
{
requires true; // requires子句
requires std::random_access_iterator<T>; // requires子句,std::random_access_iterator是一个concept
requires requires (std::size_t n) // 第一个是requires子句,后跟bool值;第二个是requires表达式,产生bool值
{
x += n;
};
}

concept

我们一般用concepts(概念)一词指称这一套C++20特性。前面介绍了各种需求,它们写起来比较长,应该用一个名字来概括它,这个名字将成为一个concept

concept的语法很简单:

template<模板参数列表>
concept 名字 = bool表达式;

bool表达式当然必须是常量表达式,通常是与模板参数列表有关的requires表达式,和其他concept的逻辑组合。concept可以产生bool值,想象一下把concept换成bool当变量模板就可以了。除此以外,concept作为concept可以用在requires子句和requires表达式中。我们稍后再来看其他用法。

concept不能递归引用自己。concept不能单独声明,所以不会出现两个concept相互引用的情况。下一节将介绍的四种约束,concept一个都不能有。

标准库定义了许多concept,分布在<concepts><iterator><ranges>中。它们中的一些与<type_traits>is_开头的类型有相同的含义,但名字不同(而且不是仅仅去掉is_)。

分类 名称 功能
语言核心 same_as 与某类型相同
derived_from 是某类型的子类
convertible_to 可以转换为某类型
common_reference_with 与某类型有common_type
common_with 与某类型有common_reference
integral 是整型
signed_integral 是带符号整型
unsigned_integral 是无符号整型
floating_point 是浮点类型
assignable_from 可从某类型赋值
swappable swap
swappable_with 可与某类型swap
destructible 可析构
constructible_from 可由某些类型的参数构造
default_initializable 可默认初始化
move_constructible 可移动构造
copy_constructible 可拷贝构造
比较 equality_comparable ==比较
equality_comparable_with 可与某类型==比较
totally_ordered 可全序比较(==<<=等)
totally_ordered_with 可与某类型全序比较
对象属性 movable 可移动和swap
copyable 可拷贝且movable
semiregular 可默认构造且copyable
regular equality_comparable && semiregular
可调用 invocable 可用某些类型的参数调用
regular_invocable invocable且无状态
predicate bool谓词
relation 是二元关系
equivalence_relation 是等价(==)关系
strict_weak_order 是严格弱序(<)关系

对于最后两个concept,除了有各种可调用的函数的需求以外,==运算符必须满足自反性与对称性,<运算符也类似。这些是句法上无法检查的,所以这两个concept更像是一种规约:如果模板参数被这种concept约束,那么客户调用时传入的参数就得满足这些语义需求。由于concept不能被特化,这一任务只能落到客户肩上,并且我不认为C++能进化出语义检查。

有些资料中的标准库concept是帕斯卡命名(PascalCase)的,因为最初的concept提案中是这样写的,原因可能是为了让它看起来属于新的C++20,或是与模板参数列表中类型大写的习惯一致。后来几个C++元老决定把concept换回C++标准命名法(Rename concepts to standard_case for C++20, while we still can),单词组成也略有修改。后来又有少许修改,以最新标准草稿(写作时为N4868)为准。

约束

现在到了应用concept的时候了。Constraint(约束)指定模板参数的需求,是以下需求的逻辑与:

  1. 模板参数前的concept;

    template<Concept T> // `Concept`是一个concept,下同
    void f(T);
  2. 模板参数列表后的requires子句;

    template<typename T>
    requires Concept<T>
    void f(T);
  3. 在简略函数模板声明(用auto替代模板类型,C++20特性)中,类型占位符(auto)前的concept;

    void f(Concept auto _arg);

    说来惭愧,写C++这么久,我从来没有过简写模板类型为auto的想法,明明是知道泛型lambda的。

  4. 在函数声明最后的requires子句。

    template<typename T>
    void f(T) requires Concept<T>;

这些requirements当然可以同时存在:

template<Concept1 T>
requires Concept2<T>
void f(T) requires Concept3<T>;

Concept2<T>Concept3<T>都在requires子句中,产生truefalse,任意一个为false时该实例化无效。

但是如何理解Concept1 T呢?把T插到Concept1的参数列表的最前面,这里为空,所以就是Concept1<T>。另一个应用这一规则的地方是复合需求的返回类型部分,我们写std::same_as<int>,其含义为requires std::same_as<T, int>(但是不能这么写)。

如果模板参数代入时出现了不存在的类型或变量,该约束仅仅是不被满足,而不会产生编译错误。

约束可以用于函数模板、类模板和成员函数,非模板类的非模板成员函数除外。函数模板与类模板的约束是类似的,只有满足约束时模板才能实例化;对于成员函数的约束,如果它作用于模板类的模板参数,当约束不满足时,并不是类模板不能被实例化,而是实例化后的模板类没有这个成员函数:

#include <concepts>

template<std::regular T>
struct Container
{
template<std::same_as<int> U>
void f(U u) { } void g()
requires std::same_as<T, int>
{ }
}; int main()
{
Container<int> ci;
ci.f(1);
ci.g();
Container<double> cd;
cd.f(1);
cd.g(); // error
}

像特化和偏特化一样,concept之间存在的包含关系也能用于重载决议——如果A成立则B一定成立,那么实例化时会优先匹配B的那一个实现。但是,concept的包含关系有时会不符合直觉,即两个concept看似包含却不能被编译器发现:

template<class T> constexpr bool is_meowable = true;
template<class T> constexpr bool is_cat = true; template<class T>
concept Meowable = is_meowable<T>; template<class T>
concept BadMeowableCat = is_meowable<T> && is_cat<T>; template<class T>
concept GoodMeowableCat = Meowable<T> && is_cat<T>; template<Meowable T>
void f1(T); // #1 template<BadMeowableCat T>
void f1(T); // #2 template<Meowable T>
void f2(T); // #3 template<GoodMeowableCat T>
void f2(T); // #4 void g(){
f1(0); // error, ambiguous:
// the is_meowable<T> in Meowable and BadMeowableCat forms distinct
// atomic constraints that are not identical (and so do not subsume each other) f2(0); // OK, calls #4, more constrained than #3
// GoodMeowableCat got its is_meowable<T> from Meowable
}

如果Meowable<T>,那么一定有is_meowable<T>,所以BadMeowableCat<T>也满足,为什么不能判断出MeowableBadMeowableCat之间的包含关系呢?包含关系作用在由&&||连接的逻辑表达式上(实际上是合取与析取),通过深入到判断两个原子的(不是&&||连接的)表达式是否相同从而决定包含关系,而只有相同的concept加上相同的模板参数才是相同,其他表达式即使再长得一样也是不同的。

在上面的例子中,编译器认为BadMeowableCat中的is_meowableMeowable中的那个不一样,从而两个concept之间没有包含关系,于是f1的重载决议就是二义的;而GoodMeowableCat显然包含了Meowable,所以对f2的调用就是合法的。

另一方面,包含关系的检查一定会深入到最底层的concept,所以没有必要给所有自定义的concept进行非常严格的层次划分。但是有一点是原则性的,就是当你需要不同约束程度的concept时,它们的最底层必须都被有名字的concept封装起来。<type_traits>里有那么多变量模板,<concepts>还要分别用不同的、有些混淆性的名字包装一下,正是因为这个。

模板升级

面向过程、基于对象、面向对象、泛型和函数式这几个编程范式是逐渐加入C++的。起初,C++并没有模板,直到1990年。Bjarne Stroustrup对模板的要求是(以下翻译了跟没翻一样):

  • Full generality/expressiveness

  • Zero overhead compared to hand coding

  • Well-specified interfaces

后来的实现满足了前两条:针对第一条,C++模板是图灵完全的;针对第二条,C++模板带来更好的运行时性能(相比于qsort或虚函数这一类实现);唯独第三条没有解决,导致冗长的模板错误,并且衍生出以SFINAE为代表的一些奇技淫巧。它们贯穿我之前写的<functional>系列,成功劝退了很多读者。

C++20带来了解决方案——concept与约束。实际上concept早在零几年就出现在C++标准的草稿里了,但在2009年被删除,没有进入C++11(这一套工具非常复杂,C++20中只是它的简化版)。后来组委会又尝试了concepts lite,但也没有进入C++17。与此同时有一条支线concepts TS在发展,并在GCC中实现了出来,以此积累经验。C++20中的concept与TS还有一定区别,是总结了concept的各种实现以后选择的。

现在我们就来看一下concept如何给模板编程进行升级。以下例子来自meds::function,是我为一个华丽而无用的单片机项目写的库。

Tag Dispatching

首先是还讲点道理的tag dispatching。S是用来放对象的空间的类型,T是要放的对象的类型,一个T能否放进一个S将决定initialize等一系列操作的方法,而object_manager对外提供一个接口,在内部进行分类讨论:

template<typename S, typename T>
class object_manager
{
private:
using local_storage = std::integral_constant<bool,
std::is_trivially_copy_constructible<T>::value
&& sizeof(T) <= sizeof(S)
&& alignof(S) % alignof(T) == 0
>; public:
static void initialize(S* _tar, T&& _obj)
{
initialize(_tar, std::move(_obj), local_storage());
} private:
static void initialize(S* _tar, T&& _obj, std::true_type )
{
new (reinterpret_cast<T*>(_tar)) T(std::move(_obj));
} static void initialize(S* _tar, T&& _obj, std::false_type)
{
_tar->template reinterpret_as<T*>() = new T(std::move(_obj));
}
};

T可以放进S时,local_storage将成为true_type,匹配到第二个initialize,反之则为第三个。

这种操作还可以接受,但有了concept以后会更好:

template<typename S, typename T>
concept locally_storable = std::is_trivially_copy_constructible<T>::value
&& sizeof(T) <= sizeof(S)
&& alignof(S) % alignof(T) == 0; template<typename S, typename T>
class object_manager
{
public:
static void initialize(S* _tar, T&& _obj)
{
reinterpret_cast<T*&>(*_tar) = new T(std::move(_obj));
} static void initialize(S* _tar, T&& _obj) requires locally_storable<S, T>
{
new (reinterpret_cast<T*>(_tar)) T(std::move(_obj));
}
};

SFINAE

然后就是不讲章法的SFINAE了。下面我们要根据一个类的可比较性调用不同实现,分为两步:function_eq_comp中定义了value指示模板参数T类型的两个实例是否可以用operator==比较,function_object_compare根据其结果执行不同操作。

template<typename T>
class function_eq_comp
{
private:
using one = int;
struct two
{
one unused[2];
}; template <typename U,
typename = decltype(std::declval<U>() == std::declval<U>())>
static one test(int);
template <typename>
static two test(...); public:
static constexpr bool value = sizeof(decltype(test<T>(0))) == sizeof(one);
}; template<typename T>
typename std::enable_if< function_eq_comp<const T&>::value, bool>::type
function_object_compare(const T& _lhs, const T& _rhs)
{
return _lhs == _rhs;
} template<typename T>
typename std::enable_if<!function_eq_comp<const T&>::value, bool>::type
function_object_compare(const T& _lhs, const T& _rhs)
{
return false;
}

==运算符可用时,one test(int)函数正确定义,test函数的返回类型将会是onevaluetrue,否则one test(int)错误,根据SFINAE,test的调用落入two test(...)valuefalse

当两个const T&不可比较时,function_eq_comp<const T&>::valuefalsestd::enable_if没有定义type,第一个function_object_compare的模板类型发生错误,根据SFINAE,该重载被忽略;与此同时第二个是可用的。反之,会调用到第一个。与tag dispatching中true_typefalse_type并列出现类似,function_eq_comp<const T&>::value与它取!的表达式也都得出现,不能像上面的concept实现那样利用两个函数之间由重载优先级建立起的层次关系。与上一节相比,这里的代码重复更恶心一点。

concept写会好看很多,尤其是在检查operator==可以用std::equality_comparable的前提下:

template<typename T>
bool function_object_compare(const T& _lhs, const T& _rhs)
{
return false;
} template<typename T>
bool function_object_compare(const T& _lhs, const T& _rhs)
requires std::equality_comparable<const T&>
{
return _lhs == _rhs;
}

思考题

  1. 下面这段代码错在哪?

    template<typename T, typename U>
    requires (T t, U u) { t + u; }
    auto add(T t, U u)
    {
    return t + u;
    }

* 2. 查阅资料,写出一个嵌套需求接受但templaterequires子句不接受的表达式。(这道题没什么意义,只是想让你去查点资料。)

  1. 不查阅资料,判断std::derived_from的两个参数(基类、子类)哪个在前,并给出判断依据。

  2. 如何给一个函数添加约束,使得它能接受任意数量的相同类型的参数?

  3. 试用concept改写一个void_t技巧的实例。

扩展阅读

Constraints and concepts

C++20: Two Extremes and the Rescue with Concepts等一系列文章

Does constraint subsumption only apply to concepts?

The tightly-constrained design space of convenient syntaxes for generic programming

C++20初体验——concepts的更多相关文章

  1. Xamarin.iOS开发初体验

    aaarticlea/png;base64,iVBORw0KGgoAAAANSUhEUgAAAKwAAAA+CAIAAAA5/WfHAAAJrklEQVR4nO2c/VdTRxrH+wfdU84pW0

  2. 在同一个硬盘上安装多个 Linux 发行版及 Fedora 21 、Fedora 22 初体验

    在同一个硬盘上安装多个 Linux 发行版 以前对多个 Linux 发行版的折腾主要是在虚拟机上完成.我的桌面电脑性能比较强大,玩玩虚拟机没啥问题,但是笔记本电脑就不行了.要在我的笔记本电脑上折腾多个 ...

  3. python--爬虫入门(七)urllib库初体验以及中文编码问题的探讨

    python系列均基于python3.4环境 ---------@_@? --------------------------------------------------------------- ...

  4. python窗体——pyqt初体验

    连续两周留作业要写ftp的作业,从第一周就想实现一个窗体版本的,但是时间实在太短,qt零基础选手表示压力很大,幸好又延长了一周时间,所以也就有了今天这篇文章...只是为了介绍一些速成的方法,还有初学者 ...

  5. iOS7初体验(3)——图像资源Images Assets

    开始之前,首先回顾一下iOS7初体验(1)——第一个应用程序HelloWorld中的一张图,如下所示: 本文便分享一下Images.xcassets的体验~_~ 1. 打开此前使用过的HelloWor ...

  6. node.js 初体验

    node.js 初体验 2011-10-31 22:56 by 聂微东, 174545 阅读, 118 评论, 收藏, 编辑 PS: ~ 此篇文章的进阶内容在为<Nodejs初阶之express ...

  7. 【阿里云产品公测】结构化数据服务OTS之JavaSDK初体验

    [阿里云产品公测]结构化数据服务OTS之JavaSDK初体验 作者:阿里云用户蓝色之鹰 一.OTS简单介绍 OTS 是构建在阿里云飞天分布式系统之上的NoSQL数据库服务,提供海量结构化数据的存储和实 ...

  8. Oracle SQL篇(一)null值之初体验

           从我第一次正式的写sql语句到现在,已经超过10年的时间了.我写报表,做统计分析和财务对账,我一点点的接触oracle数据库,并尝试深入了解.这条路,一走就是10年,从充满热情,到开始厌 ...

  9. 文档数据库RavenDB-介绍与初体验

    文档数据库RavenDB-介绍与初体验 阅读目录 1.RavenDB概述与特性 2.RavenDB安装 3.C#开发初体验 4.RavenDB资源 不知不觉,“.NET平台开源项目速览“系列文章已经1 ...

随机推荐

  1. 深入web workers (上)

    前段时间,为了优化某个有点复杂的功能,我采用了shared workers + indexDB,构建了一个高性能的多页面共享的服务.由于是第一次真正意义上的运用workers,比以前单纯的学习有更多体 ...

  2. Go语言如何像foreach一样有序遍历map

    目录 问题 解决 给key排序思路 开源实现 问题 Go语言的Map是无序遍历的,遍历一个map代码如下 package main import ( "fmt" ) func ma ...

  3. Pandas_分组与聚合

    # 分组统计是数据分析中的重要环节: # 1-数据分组:GroupBy的原理和使用方法: # 2-聚合运算:学会分组数据的聚合运算方法和函数使用: 类似于 SQL思想 # 3-分组运算:重点 appl ...

  4. CSS两列布局的多种方式

    两列布局(一侧固定宽度,一侧自适应),在工作中应该是经常使用到,可以说是前端基础了.这种两列布局的样式是我们在平时工作中非常常见的设计,同时也是面试中要求实现的高频题.很有必要掌握以备不时之需.这里总 ...

  5. linux 源码下载和在线查看网站

    下载: https://www.kernel.org/ 查看: https://elixir.bootlin.com/linux/

  6. centos6 virbox安装

    yum install kernel-devel yum update kernel* wget http://download.virtualbox.org/virtualbox/debian/or ...

  7. 一文带你玩转对象存储COS文档预览

    随着"互联网+"的发展,各行各业纷纷"去纸化",商务合同.会议纪要.组织公文.商品图片.培训视频.学习课件.随堂讲义等电子文档无处不在.而要查看文档一般需要先下 ...

  8. spring中的事务有两种方式

    1种是我们常用的声明式事务,如注解,或者配置文件配置的. 2种是编程式事务,如 TransactionTemplate 类的使用.

  9. SpringBoot Redis切换数据库遇到的坑

    项目不同业务的redis数据存在不同的库中,操作数据需要切换redis库,在网上找了一段代码,确实可以切换数据库.但是使用一段时间后发现部分数据存储的数据库不正确,排查后发现setDatabase是线 ...

  10. K8S环境的Jenkin性能问题描述

    Return Homezq2599 CnBlogsHomeContactAdminPosts - 75 Articles - 0 Comments - 16 K8S环境的Jenkin性能问题处理 环境 ...