C++ 很有趣:最优字母顺序






4.94/5 (25投票s)
本文旨在表明,用 C++ 编写代码可以像用其他主流语言一样高效和有趣。
概述
本文是我们计划系列文章的第一篇,旨在表明,尽管 Windows 平台上的开发者普遍(误)认为 C++ 编程很难,尤其是对于那些不太熟悉甚至完全不熟悉这门语言的开发者来说,但 C++ 编程并不比 Java、C#、VB.NET、Ruby、Python 等其他语言更难或更有趣。本系列文章主要面向(但不仅限于)不太熟悉 C++,但可能在其他编程语言方面经验丰富的开发者。
在本文中,我将展示如何用 C++ 轻松解决一个(相当)数学化的问题。虽然可能有多种方法,或多或少最优,但在本例中,我将不专注于从算法中获得最佳性能。许多人认为 C++ 只应在必须注重性能,并且必须最大限度地利用内存或时钟周期的场合使用。虽然 C++ 在这方面确实名列前茅,但我们不能忘记 C++ 是一种通用的编程语言,可以用于多种目的。我写本文的目的是展示如何轻松地利用容器、算法、随机数生成器、任务、时钟等来实现一个简单的解决方案来解决提出的问题。
我将使用 Visual Studio 2012 来编写程序。我假设您知道如何创建一个 C++ 控制台应用程序并编写一个 main()
函数(C++ 程序的入口点),以便您能够理解、编译和运行代码。
问题
我要解决的问题是 Ross Eckler 在其著作《Making the Alphabet Dance: Recreational Wordplay》中提出的。这个问题被称为“最优字母顺序”,并且在此 处有更详细的描述。
假设我们拥有包含所有英语单词的列表。根据英语中字母表的正常排序(A、B、C、...、Z),有一些单词的所有字母都按字母顺序排列(例如,DEEP、FLUX 和 CHIMPS)。然而,如果我们改变字母表的排序,我们将改变具有“字母顺序”字母的单词的数量。哪种字母排列方式可以使这个数量最大化?哪种方式又可以使它最小化?
一个包含 67230 个单词的列表可用于测试此解决方案,该列表可在此 处找到。此列表中的 860 个单词使用拉丁字母表,其字母顺序排列。
解决方案
首先要考虑的是如何表示字母表。可能有多种选择(各有优缺点)。它可以是一个包含 26 个字符的数组或向量,甚至可以是一个 string
。为了简化确定一个字母在特定字母表中是否排在另一个字母之前,我将使用一个映射,将每个字母与其在字母表中的索引关联起来。拉丁字母表将如下所示:
'A' => 0
'B' => 1
...
'Z' => 25
要使用映射,我们必须包含 <map>
头文件。为了避免在许多地方冗余地编写 std::map<char, int>
,我将定义一个名为 alphabet_t
的类型。
#include <map>
typedef std::map<char, int> alphabet_t;
以下名为 base_alphabet()
的方法创建一个该类型的实例,并用对应于拉丁字母表的值对其进行初始化。
alphabet_t base_alphabet()
{
alphabet_t alpha;
for(int i = 0; i < 26; ++i)
alpha.insert(std::make_pair(static_cast<char>('A' + i), i));
return alpha;
}
注意,本文档中使用的所有容器、算法和类型都包含在标准模板库(STL,由 C++ 标准定义,所有编译器都提供该库)的 std
命名空间中。我们可以使用 using namespace std;
指令将此命名空间中的所有符号导入全局命名空间。但是,为了清晰起见,我将不使用它,而是选择使用 std::
命名空间限定符来完全限定所有类型和函数。
仅出于娱乐和测试的目的,以下函数会生成一个字母表,该字母表是拉丁字母表的反序(Z 是第一个字母,A 是最后一个字母)。
alphabet_t reversed_alphabet()
{
alphabet_t alpha;
for(int i = 25; i >=0; i--)
alpha.insert(std::make_pair(static_cast<char>('Z' - i), i));
return alpha;
}
第一个便捷的函数是能够将此映射转换(或折叠)为 string
(可以与其他 string
进行比较或打印到控制台)的函数。这样的方法应该接受一个 alphabet_t
对象并返回一个 std::string
。我将继续编写该函数的测试。
#include <assert.h>
void test_alphabet_string()
{
auto alpha1 = base_alphabet();
auto alphastr1 = alphabet_string(alpha1);
assert(alphastr1 == "ABCDEFGHIJKLMNOPQRSTUVWXYZ");
auto alpha2 = reversed_alphabet();
auto alphastr2 = alphabet_string(alpha2);
assert(alphastr2 == "ZYXWVUTSRQPONMLKJIHGFEDCBA");
}
注意 auto
关键字是类型的占位符,它告诉编译器从赋值运算符右侧的表达式推断出类型。
此转换函数将必须按照其索引的递增顺序将字母放入 string
中。为了简化这一点,我将使用第二个映射,将字母表中的每个索引与一个字母关联起来。对于拉丁字母表,这意味着:
0 => 'A'
1 => 'B'
...
25 => 'Z'
我将以与定义 alphabet_t
相同的方式定义第二个类型。
typedef std::map<int, char> inversed_alphabet_t;
名为 inverse_alphabet_map
的函数接受一个 alphabet_t
并返回一个 inversed_alphabet_t
。它遍历 alphabet_t
对象的所有元素,并将每个键值对作为值-键对插入到第二个映射中。
inversed_alphabet_t inverse_alphabet_map(alphabet_t const & alphabet)
{
inversed_alphabet_t inversedalpha;
for(auto const & elem : alphabet)
inversedalpha.insert(std::make_pair(elem.second, elem.first));
return inversedalpha;
}
回到 alphabet_string
函数,它接受一个 alphabet_t
对象,生成一个具有反向键值对的新映射,然后使用 std::transform
来生成字符串。std::transform
是一个通用算法,位于 <algorithm>
头文件中,它对一个范围应用一个操作并将结果存储在另一个范围(可能是相同的范围)中。我提供给算法的(一元)操作是一个 lambda 函数,它接受一个 std::pair<int, char>
(inversed_alphabet_t
的值类型)并返回一个字符。
#include <string>
#include <algorithm>
std::string alphabet_string(alphabet_t const & alphabet)
{
std::string alphabet_string(alphabet.size(), ' '); // initialize a string of
// alphabet.size()
// space (' ') elements
auto reversed = inverse_alphabet_map(alphabet);
std::transform(
begin(reversed), end(reversed), // range to work on
begin(alphabet_string), // beginning of the range
// where the result is stored
[](std::pair<int, char> const & elem){return elem.second;} // applied operation
);
return alphabet_string;
}
注意,字母表以常量引用的形式传递给函数。与按值传递相比,按引用传递可以防止对象被复制。将参数标记为 const
可以防止我们在函数内部修改该对象(任何此类尝试都会被编译器标记为错误)。所有在函数内部不应修改的参数都应按常量传递。
从 main
调用 test_alphabet_string
函数不应触发断言。
int main()
{
test_alphabet_string();
return 0;
};
如果我们某个函数或测试中存在错误,我们会收到断言。例如:对于基本(拉丁)字母表,预期的 string
是“BAC...Z”,而不是“ABC...Z”。在这种情况下,将触发断言。
auto alpha1 = base_alphabet();
auto alphastr1 = alphabet_string(alpha1);
assert(alphastr1 == "BACDEFGHIJKLMNOPQRSTUVWXYZ");
您可以将 Visual Studio 的本机调试器附加到进程,然后按“重试”。通过浏览调用堆栈,您可以到达代码中触发断言的那一行。
接下来我将编写一个交换字母表的两个字母的函数。因此,如果我们有一个字母表“ABC...Z”,并将‘A’与‘B’交换,我们得到的字母表是“BAC...Z”。该函数的测试可能如下所示(测试边缘和中间字母)。
void test_swap_letter()
{
auto alpha1 = base_alphabet();
swap_letter(alpha1, 'A', 'B');
auto alphastr1 = alphabet_string(alpha1);
assert(alphastr1 == "BACDEFGHIJKLMNOPQRSTUVWXYZ");
auto alpha2 = base_alphabet();
swap_letter(alpha2, 'Y', 'Z');
auto alphastr2 = alphabet_string(alpha2);
assert(alphastr2 == "ABCDEFGHIJKLMNOPQRSTUVWXZY");
auto alpha3 = base_alphabet();
swap_letter(alpha3, 'A', 'Z');
auto alphastr3 = alphabet_string(alpha3);
assert(alphastr3 == "ZBCDEFGHIJKLMNOPQRSTUVWXYA");
auto alpha4 = base_alphabet();
swap_letter(alpha4, 'I', 'O');
auto alphastr4 = alphabet_string(alpha4);
assert(alphastr4 == "ABCDEFGHOJKLMNIPQRSTUVWXYZ");
}
swap_letter
函数非常简单。它使用 <algorithm>
头文件中的 std::swap
算法来交换映射中对应于提供的键的值。
void swap_letter(alphabet_t & alphabet, char const l1, char const l2)
{
std::swap(alphabet[l1], alphabet[l2]);
}
实现的一个关键方面是,给定一个特定的字母表映射,找出单词是否已排序。is_ordered
函数接受一个代表单词的 string
和一个 alphabet_t
,并返回一个 bool
。我将首先为该函数编写一个简单的测试。
void test_is_ordered()
{
auto alpha1 = base_alphabet();
assert(true == is_ordered("ABCD", alpha1));
assert(true == is_ordered("AABCDXYZ", alpha1));
assert(false == is_ordered("AACB", alpha1));
swap_letter(alpha1, 'A', 'B');
assert(false == is_ordered("ABCD", alpha1));
assert(false == is_ordered("AABCDXYZ", alpha1));
assert(true == is_ordered("BAC", alpha1));
assert(true == is_ordered("BBAAC", alpha1));
assert(false == is_ordered("BCA", alpha1));
}
bool is_ordered(std::string const & word, alphabet_t const & alphabet)
{
if(word.size() < 2) return true;
for(size_t i = 1; i < word.size(); ++i)
{
auto lpos1 = alphabet.find(word[i]);
auto lpos2 = alphabet.find(word[i-1]);
if(lpos1->second < lpos2->second)
return false;
}
return true;
}
下一步是确定给定字母表对单词列表的得分。alphabet_score
函数接受一个代表单词的 string
向量和一个 alphabet_t
,并返回在该给定字母表中已排序的单词数量。一个简单的测试函数可能看起来像这样:
void test_alphabet_score()
{
std::string arrwords[] = {"THIS", "IS", "A",
"SIMPLE", "ALPHABET", "SCORE", "TEST"};
std::vector<std::string> words(begin(arrwords), end(arrwords));
assert(2 == alphabet_score(words, base_alphabet()));
}
alphabet_score
使用 <algorithm>
头文件中的 std::count_if
算法来计算范围内满足特定条件的元素数量。提供的谓词是一个 lambda 函数,它接受一个代表单词的 string
并调用 is_ordered
来检查单词在给定字母表中的排序。
#include <string>
int alphabet_score(std::vector<std::string> const & words, alphabet_t const & alphabet)
{
return std::count_if(
begin(words), end(words),
[&alphabet](std::string const & word) {return is_ordered(word, alphabet);});
}
在文章开头,我提到了一个包含 67230 个单词的编译列表,其中在拉丁字母表中包含 860 个排序单词。我们可以进一步测试 alphabet_score
,增加一个附加测试。
assert(860 == alphabet_score(read_words(), base_alphabet()));
read_words
函数使用 <fstream>
头文件中的 std::ifstream
文件流来读取文件,并将读取的单词存储在 string
向量中。
std::vector<std::string> read_words()
{
std::ifstream file("words.txt");
std::istream_iterator<std::string> start(file), end;
std::vector<std::string> words(start, end);
return words;
}
此时,我们已具备测试给定字母表单词列表得分的所有条件。接下来是找到一个得分最高的最佳字母表。为此,我们将使用 爬山算法:我们从一个字母表开始(可以是拉丁字母表或随机字母表),然后计算得分。然后,我们通过交换两个随机键来修改字母表。如果新字母表的得分更高,我们就将该得分存储为迄今为止的最佳得分。每迭代 1000 次,我们会更改两次字母表,每迭代 5000 次,我们会更改三次字母表。为了可能在新局部最大值出现时提升搜索效率,每迭代 50000 次,我们会对整个字母表进行随机洗牌。该算法可以针对给定的迭代次数或给定的时间间隔运行。
在这个描述中,我们缺少用于通过交换两个随机字母来更改字母表以及完全随机洗牌字母表(这意味着某些字母在新的字母表映射中可能保持其位置)的函数。
为了能够选择随机字母,我们将使用 <random>
头文件中提供的设施。STL 支持使用生成器(生成均匀分布的数字)和分布(将生成器产生的数字序列转换为遵循特定分布的序列)来生成(伪)随机数。从这个头文件中,我们将使用 mt19937
Marsenne Twister 生成器和 uniform_int_distribution
来生成 [0, 25] 范围内的均匀分布随机数。
第一个函数 alter_alphabet
接受一个 alphabet_t
、一个 mt19937
生成器和一个 uniform_int_distribution
。它在 'A'...'Z' 范围内选择两个不同的字母并交换它们在字母表中的位置。
#include <random>
void alter_alphabet(alphabet_t & alphabet, std::mt19937 & generator,
std::uniform_int_distribution<int> & uniform_distribution)
{
auto l1 = 'A' + uniform_distribution(generator);
auto l2 = 0;
do {
l2 = 'A' + uniform_distribution(generator);
} while(l2 == l1);
swap_letter(alphabet, l1, l2);
}
第二个函数 shuffle_alphabet
接受一个字母表,并使用 <algorithm>
头文件中的 std::random_shuffle
算法随机洗牌其字母。此算法不适用于映射,因此首先要做的是将 alphabet_t
的值(字母在字母表中的索引)复制到一个 vector
中。然后对该向量进行洗牌,并将其值复制回映射。
void shuffle_alphabet(alphabet_t & alphabet)
{
std::vector<alphabet_t::mapped_type< values(alphabet.size());
std::transform(
begin(alphabet), end(alphabet),
begin(values),
[](alphabet_t::value_type const & elem) {return elem.second;});
std::random_shuffle(begin(values), end(values));
auto crtvalue = begin(values);
for(auto & elem : alphabet)
{
elem.second = *crtvalue++;
}
}
找到最佳字母表的实现遵循上述描述。函数 find_optimal_alphabet
接受一个代表单词的 string
向量和一个代表算法应停止的时间(以秒为单位)的整数,并返回一个整数对,代表最佳得分,以及一个 string
,代表最佳得分对应的字母表排序。
#include <chrono>
std::pair<int, std::string> find_optimal_alphabet(std::vector<std::string>
const & words, int const timeout)
{
std::mt19937 rndgenerator(static_cast<unsigned int>
(std::chrono::system_clock::now().time_since_epoch().count()));
std::uniform_int_distribution<int> distribution(0,25);
auto bestscore = std::make_pair<int, std::string>(0, "");
auto alpha = base_alphabet();
int iteration = 0;
auto start_time = std::chrono::system_clock::now();
while(true)
{
auto crt_time = std::chrono::system_clock::now();
if(std::chrono::duration_cast<std::chrono::seconds>(crt_time-start_time) >
std::chrono::seconds(timeout))
break;
auto score = alphabet_score(words, alpha);
if(score > bestscore.first)
{
bestscore.first = score;
bestscore.second = alphabet_string(alpha);
std::cout << score << " \t" << alphabet_string(alpha)
<< " iteration=" << iteration << std::endl;
}
alter_alphabet(alpha, rndgenerator, distribution);
iteration++;
if((iteration % 1000) == 0)
alter_alphabet(alpha, rndgenerator, distribution);
if((iteration % 5000) == 0)
alter_alphabet(alpha, rndgenerator, distribution);
if((iteration % 50000) == 0)
shuffle_alphabet(alpha);
}
return bestscore;
}
注意,为了播种 Marsenne Twister 随机数生成器,我们使用了 <chrono>
头文件中可用的 std::chrono::system_clock
,它代表系统范围的实时时钟。同一个 system_clock
也用于确定算法的运行时间。此头文件中存在多个时钟,包括 high_resolution_clock
,它是一个具有最小滴答周期的时钟。对于本示例,系统时钟就足够了。
程序的 main
函数将如下所示:
int main()
{
auto bestscore = find_optimal_alphabet(read_words(), 60);
std::cout << "best score:" << std::endl;
std::cout << bestscore.first << " \t" << bestscore.second << std::endl;
return 0;
}
我们得到的输出可能如下所示(由于字母表更改的随机性,每次运行时都应有所不同)。
860 ABCDEFGHIJKLMNOPQRSTUVWXYZ iteration=0
1106 PBCDEFGHIJKLMNOAQRSTUVWXYZ iteration=1
1151 PBRDEAGHMJKLINOFQCSTVUWXYZ iteration=5
1170 TBCVQKGOHUWFDJRPSAXLYMZINE iteration=103
1227 FQZOCUKRNYPWLIAMHXJTVGESDB iteration=289
1293 FQZOCUKRNYPWAILMHXJTVGESDB iteration=290
1353 FQROCUKZNYPWAILMHXJTVGEDSB iteration=292
1364 FCRONUKZQYPWAILMHXETVGJDSB iteration=295
1484 CIMXPFOHUABQWZNDYRTLEGJVKS iteration=887
1497 CIBXPFOHUAMQWZNDYRTLEGJVKS iteration=888
1566 CIBXRFOHUAMQWZNDYPTLEGJVKS iteration=889
1598 BCTFAZNXLRHOIMQGUEKPWJSDVY iteration=2009
1766 BCJFAZNXLRHOIMQGUEKPWTSDVY iteration=2010
1819 BCJFAZNXLOURIMQGHEKPWTSDVY iteration=2012
1896 PGBOCHXRVLAKUDIEMWJZNTFQSY iteration=11955
best score:
1896 PGBOCHXRVLAKUDIEMWJZNTFQSY
提高性能
编写上述解决方案并不难,但有一个缺点:它以顺序方式运行,在单个线程中,使用 CPU 的单个核心。能够并行多次运行此算法,有望找到更优的字母表。
为了能够从不同线程调用该函数而需要进行的更改非常少。我们应该删除打印到控制台(这是一个共享资源,需要同步)。但是,一个关键方面是播种 Marsenne Twister 随机数生成器。之前的实现使用了当前时间(std::chrono::system_clock::now().time_since_epoch().count()
)来初始化算法。如果我们短时间内多次调用此函数,我们可能会面临用相同的值初始化不同线程中的算法的风险,结果是所有这些生成器将产生相同的数字序列。为避免这种情况,我们将不得不确保用不同的种子对其进行初始化。因此,我们将种子作为函数的参数。其余部分基本相同。
std::pair<int, std::string> find_optimal_alphabet_async
(std::vector<std::string> const & words, int const timeout, int const seed)
{
std::mt19937 rndgenerator(seed);
std::uniform_int_distribution<int> distribution(0,25);
auto bestscore = std::make_pair<int, std::string>(0, "");
auto alpha = base_alphabet();
shuffle_alphabet(alpha);
int iteration = 0;
auto start_time = std::chrono::system_clock::now();
while(true)
{
auto crt_time = std::chrono::system_clock::now();
if(std::chrono::duration_cast<std::chrono::seconds>
(crt_time-start_time) > std::chrono::seconds(timeout))
break;
auto score = alphabet_score(words, alpha);
if(score > bestscore.first)
{
bestscore.first = score;
bestscore.second = alphabet_string(alpha);
}
alter_alphabet(alpha, rndgenerator, distribution);
iteration++;
if((iteration % 1000) == 0)
alter_alphabet(alpha, rndgenerator, distribution);
if((iteration % 5000) == 0)
alter_alphabet(alpha, rndgenerator, distribution);
if((iteration % 50000) == 0)
shuffle_alphabet(alpha);
}
return bestscore;
}
改变更大的是 main
函数。在这里,我们将创建多个任务,每个任务在一个单独的线程中执行 find_optimal_alphabet_async
函数。该函数通过调用 <future>
头文件中的 std::async
函数来异步执行。函数调用的结果存储在 std::future<T>
(来自同一个头文件)中。创建任务后,我们尝试获取每个函数调用的结果。get()
函数会阻塞,直到结果可用。我们查看所有函数的结果,并选择最佳得分。
#include <future>
int main()
{
auto words = read_words();
int timeout = 60;
std::vector<std::future<std::pair<int, std::string>>> futures(8);
unsigned int seed = static_cast<unsigned int>
(std::chrono::system_clock::now().time_since_epoch().count());
unsigned int index = 0;
for(auto & f : futures)
{
f = std::async(std::launch::async, [&]()
{return find_optimal_alphabet_async(words, timeout, seed + index++);});
}
auto bestscore = std::make_pair<int, std::string>(0, "");
for(auto & f : futures)
{
auto score = f.get();
std::cout << score.first << " \t" << score.second << std::endl;
if(score.first > bestscore.first)
bestscore = score;
}
std::cout << "best score:" << std::endl;
std::cout << bestscore.first << " \t" << bestscore.second << std::endl;
return 0;
}
以下是运行此并行算法版本(也运行 60 秒)可能得到的结果。如您所见,我们取得了一些改进。运行时间越长,分数就越高。
1842 VJDTPUCHWROZANBKIMGQYFELSX
2002 BRMOZPTWYXHAJIVULNKQCEGDSF
1887 MBCPYDOVQRWULGAZTKJIFNEXSH
2097 FGQHCUWBJAONVXILKRETPDYMZS
1926 BWRZPCVGHQUOAMIYXFJTLESDNK
1826 QPFYBCJVHLUTEMXOAWIKZNRGDS
1887 CFDGBWZHMUAIVLJQTKYXOPENRS
2032 PDBOMWCAIUFJHLNZVKEYXGRTSQ
best score:
2097 FGQHCUWBJAONVXILKRETPDYMZS
如果您查看 CPU 加载,您会发现此算法使用了 100% 的 CPU。
如果您使用 Visual Studio 的 Premium 或 Ultimate 版本,它们会提供一个 并发可视化工具,让您可以检查多线程应用程序的性能。这是我们应用程序的几张屏幕截图。
编写单元测试
也许你们中的一些人会说我写的测试并非真正的单元测试。我只是写了一些在 main
中调用的函数。我污染了我的代码。这是真的。但这并不意味着没有单元测试框架,因此您可以创建与实际代码分开的单元测试,并在构建应用程序时、进行更改时或任何您想要的时候运行它们。有各种这样的单元测试框架,包括 Visual Studio 中提供的。但要使用它,我们必须对项目进行一些重构。我将把上面提供的解决方案的核心功能移到一个单独的 Win32 DLL 项目中,然后将其与主项目和单元测试项目链接。
Win32DLL
项目将包含一个头文件(如下所示),其中声明了模块导出的函数;一个包含这些函数实现的源文件;一个 dllmail.cpp 文件,其中包含 DllMain
函数(DLL 的入口点);以及一个空的导出定义文件(这样项目也会生成一个我们可以从主项目链接的静态库)。
#pragma once
#include <string>
#include <map>
#include <vector>
#include <random>
#ifdef ALPHAORDER_DLL
#define EXPORTAPI __declspec(dllexport)
#else
#define EXPORTAPI __declspec(dllimport)
#endif
typedef std::map<char, int> alphabet_t;
typedef std::map<int, char> inversed_alphabet_t;
EXPORTAPI alphabet_t base_alphabet();
EXPORTAPI alphabet_t reversed_alphabet();
EXPORTAPI inversed_alphabet_t inverse_alphabet_map(alphabet_t const & alphabet);
EXPORTAPI std::string alphabet_string(alphabet_t const & alphabet);
EXPORTAPI bool is_ordered(std::string const & word, alphabet_t const & alphabet);
EXPORTAPI int alphabet_score(std::vector<std::string> const & words,
alphabet_t const & alphabet);
EXPORTAPI void swap_letter(alphabet_t & alphabet, char const l1, char const l2);
EXPORTAPI void alter_alphabet(alphabet_t & alphabet, std::mt19937 & generator,
std::uniform_int_distribution<int> & uniform_distribution);
EXPORTAPI void shuffle_alphabet(alphabet_t & alphabet);
EXPORTAPI std::vector<std::string> read_words();
EXPORTAPI std::pair<int, std::string> find_optimal_alphabet
(std::vector<std::string> const & words, int const timeout);
EXPORTAPI std::pair<int, std::string> find_optimal_alphabet_async
(std::vector<std::string> const & words, int const timeout, int const seed);
在主项目中,我们必须包含声明导出函数的头文件,以及一个链接到相应静态库的指令(这也可以放在项目属性中)。其余代码,即 main
函数,不需要任何额外的更改。
#include "..\OptimalAlphaOrderCore\OptimalAlphaOrderCore.h"
#pragma comment(lib, "OptimalAlphaOrderCore")
注意,您可以查看附加源代码项目中关于此解决方案重构的详细信息。
为了编写真正的单元测试并利用 Visual Studio 中可用的测试框架,我将创建一个本机单元测试项目。
这是包含三个项目的解决方案:
在编写单元测试之前,需要做两件事:包含 cppunittest.h 头文件和待测试函数的头文件。
#include "CppUnitTest.h"
using namespace Microsoft::VisualStudio::CppUnitTestFramework;
#include "..\OptimalAlphaOrderCore\OptimalAlphaOrderCore.h"
#pragma comment(lib, "OptimalAlphaOrderCore")
然后我们必须定义一个 TEST_CLASS
,其中包含 TEST_METHOD
。这些方法代表框架执行的单元测试。它们对我们已经拥有的内容只需要很少的更改。基本上,只有断言不同。下面是之前编写的四个测试方法迁移到 C++ 测试框架。
namespace OptimalAlphaOrderTests
{
TEST_CLASS(UnitTest_OptimalAlphaOrder)
{
public:
TEST_METHOD(test_alphabet_string)
{
auto alpha1 = base_alphabet();
auto alphastr1 = alphabet_string(alpha1);
Assert::AreEqual<std::string>
(alphastr1, "ABCDEFGHIJKLMNOPQRSTUVWXYZ", L"Mismatched alphabet string");
auto alpha2 = reversed_alphabet();
auto alphastr2 = alphabet_string(alpha2);
Assert::AreEqual<std::string>
(alphastr2, "ZYXWVUTSRQPONMLKJIHGFEDCBA", L"Mismatched alphabet string");
}
TEST_METHOD(test_swap_letter)
{
auto alpha1 = base_alphabet();
swap_letter(alpha1, 'A', 'B');
auto alphastr1 = alphabet_string(alpha1);
Assert::AreEqual<std::string>(alphastr1, "BACDEFGHIJKLMNOPQRSTUVWXYZ");
auto alpha2 = base_alphabet();
swap_letter(alpha2, 'Y', 'Z');
auto alphastr2 = alphabet_string(alpha2);
Assert::AreEqual<std::string>(alphastr2, "ABCDEFGHIJKLMNOPQRSTUVWXZY");
auto alpha3 = base_alphabet();
swap_letter(alpha3, 'A', 'Z');
auto alphastr3 = alphabet_string(alpha3);
Assert::AreEqual<std::string>(alphastr3, "ZBCDEFGHIJKLMNOPQRSTUVWXYA");
auto alpha4 = base_alphabet();
swap_letter(alpha4, 'I', 'O');
auto alphastr4 = alphabet_string(alpha4);
Assert::AreEqual<std::string>(alphastr4, "ABCDEFGHOJKLMNIPQRSTUVWXYZ");
}
TEST_METHOD(test_is_ordered)
{
auto alpha1 = base_alphabet();
Assert::IsTrue(is_ordered("ABCD", alpha1));
Assert::IsTrue(is_ordered("AABCDXYZ", alpha1));
Assert::IsFalse(is_ordered("AACB", alpha1));
swap_letter(alpha1, 'A', 'B');
Assert::IsFalse(is_ordered("ABCD", alpha1));
Assert::IsFalse(is_ordered("AABCDXYZ", alpha1));
Assert::IsTrue(is_ordered("BAC", alpha1));
Assert::IsTrue(is_ordered("BBAAC", alpha1));
Assert::IsFalse(is_ordered("BCA", alpha1));
}
TEST_METHOD(test_alphabet_score)
{
std::string arrwords[] = {"THIS", "IS", "A", "SIMPLE", "ALPHABET", "SCORE", "TEST"};
std::vector<std::string> words(begin(arrwords), end(arrwords));
Assert::AreEqual(2, alphabet_score(words, base_alphabet()));
Assert::AreEqual(860, alphabet_score(read_words(), base_alphabet()));
}
};
}
当您执行测试时,您会看到哪些测试成功、哪些测试失败、在哪里以及为什么失败、执行花费了多长时间等等。您可以执行不同的操作,例如重新运行失败的测试,或重新运行以前未运行的测试等。有关在 Visual Studio 中编写本机单元测试的更多详细信息,请参阅 此处。
结论
本文的目的是表明,尽管不熟悉 C++ 的人们普遍存在误解,但用 C++ 编写代码并非如此困难。用 C++ 编写不仅仅是直接管理内存(实际上近年来,使用标准化的智能指针已经使其过时),或者进行位运算符或其他潜在的吓人、低级别的编码。使用标准模板库和其他库,可以轻松地编写与使用其他主流语言相同生产力的代码。在本文中,我展示了如何使用各种容器(vector
、map
、string
)、算法(transform
、count_if
、swap
、random_suffle
)、生成随机数(mt19937
、uniform_int_distribution
)、使用时钟(chrono::system_clock
)、异步运行函数(async
、future
)等等。(我没有使用类以及通常与面向对象编程相关的其他内容,因为它们对于本文的目的来说是不必要的。)我还展示了一些 Visual Studio 中可用的工具的预览,例如并发可视化工具。最后,我向您展示了如何利用测试框架为本机项目创建和运行单元测试。
我希望本文能够让那些抱怨 C++ 代码难写的人认识到,它实际上并非如此。用 C++ 编写既可以高效又可以有趣。
历史
- 2013 年 10 月 4 日:初始版本