与此question联合。我无法为以下看似基本的问题提出一个良好的类型安全解决方案。我有一个类music_playlist,它有一个应该播放的歌曲列表。看起来非常简单,只需创建队列中所有歌曲的std :: list,并将其提供给用户。然而,出于必要,音频解码和音频呈现发生在不同的线程上。因此列表需要受互斥保护。很多时候,使用我的库的其他程序员忘记了互斥锁。这显然导致了“奇怪”的问题。
所以一开始我只是为班级编写了setter。
struct music{};
class music_playlist{
private:
std::list<music> playlist;
public:
void add_song(music &song){playlist.push_back(song);}
void clear(){playlist.clear();}
void set_list(std::list<music> &songs){playlist.assign(songs.begin(),songs.end());}
//etc
};
这导致用户代码如下......
music song1;
music song2;
music song3;
music song4;
music song5;
music song6;
music_playlist playlist1;
playlist1.add_song(song1);
playlist1.add_song(song2);
playlist1.add_song(song3);
playlist1.add_song(song4);
playlist1.add_song(song5);
playlist1.add_song(song6);
//or
music_playlist playlist2;
std::list<music> songs;
songs.push_back(song1);
songs.push_back(song2);
songs.push_back(song3);
songs.push_back(song3);
songs.push_back(song5);
songs.push_back(song6);
playlist2.set_list(songs);
虽然这有效但非常明确。输入是非常繁琐的,并且由于实际工作周围的所有残余而容易出错。为了证明这一点,我实际上故意在上面的代码中添加了一个错误,这样的东西很容易制作,并且可能不会触及代码评论,而song4永远不会在播放列表2中播放。
从那里我开始研究可变函数。
struct music{};
class music_playlist{
private:
std::list<music> playlist;
public:
void set_listA(music *first,...){
//Not guaranteed to work, but usually does... bleh
va_list Arguments;
va_start(Arguments, first);
if (first) {
playlist.push_back(first);
}
while (first) {
music * a = va_arg(Arguments, music*);
if (a) {
playlist.push_back(a);
}else {
break;
}
}
}
void set_listB(int count,music first,...){
va_list Arguments;
va_start(Arguments, first);
playlist.push_back(first);
while (--count) {
music a = va_arg(Arguments, music);
playlist.push_back(a);
}
}
//etc
};
哪会导致用户代码如下...
playlist1.set_listA(&song1,&song2,&song3,&song4,&song5,&song6,NULL);
//playlist1.set_listA(&song1,&song2,&song3,&song4,&song5,&song6); runtime error!!
//or
playlist2.set_listB(6,song1,song2,song3,song4,song5,song6);
现在更容易看出是否添加了两首歌曲或未加入。但是在解决方案A中,如果NULL不在列表的末尾,并且不是跨平台,则它将崩溃。在solutionB中,你必须计算可能导致多个错误的参数数量。此外,没有一种解决方案是类型安全的,用户可以传入不相关的类型并在运行时等待崩溃。这似乎不是一个可持续的解决方案。那么我遇到了std :: initializer_list。不能使用它我开发的几个编译器还没有它的支持。所以我试着模仿它。我最终得到了以下解决方案。
Is this use of the "," operator considered bad form?
这会导致用户代码看起来像这样......
struct music_playlist{
list<music> queue;
//...
};
int main (int argc, const char * argv[])
{
music_playlist playlist;
music song1;
music song2;
music song3;
music song4;
playlist.queue = song1,song2; // The queue now contains song1 and song2
playlist.queue+= song1,song3,song4; //The queue now contains two song1s and song2-4
playlist.queue = song2; //the queue now only contains song2
return 0;
}
这个语法并没有让我们的小测试组感到困惑。但是,我严重关注滥用运营商超载问题。所以我发布了上面的问题。我想看看哪些程序员是我们测试组的比较专家。相当多的程序员不喜欢它,但它似乎比上面的解决方案更好,因为它会在编译时捕获大多数错误而不是在运行时。然而,汤姆发布了一个有趣的反例,这将导致意外行为。
//lets combine differnt playlists
new_playlist.queue = song1 //the first playlist
,(song3,song4) //the second playlist //opps, I didn't add song 3!
, song5;
这让我感到厌烦。那你对更好的解决方案有什么想法吗?
P.S。以上代码都没有被编译,它们只是用于示例目的。
答案 0 :(得分:6)
首先想到的问题是这是否是一个问题,以及这是否是值得解决的问题。由于您正在创建界面,因此您的客户端将是用户代码(不是人,代码)。您的客户在代码中硬编码播放列表会多少次,比如说,商店从文件中加载它们还是从用户选择中构建它们?
考虑该功能的实际价值,并将其与它们如何使用您的库的影响进行比较,并考虑用户可能对更改的界面有多少问题。
最简单的解决方案是接受迭代器来构建/重置列表,然后让用户处理问题。当然,他们可以按照您展示的方式构建自己的容器,但他们也可以使用 Boost.Assignment 来处理样板,因此他们的用户代码如下所示:
std::vector<music> songs = boost::assign::list_of()( song1 )( song2 );
play_list.set( songs.begin(), songs.end() );
或者,如果他们对该库不满意,可以使用普通的旧数组:
music songs[2] = { song1, song2 };
play_list:set( songs, songs + 2 ); // add your preferred magic to calculate the size