【问题标题】:What is a good typesafe alternative to variadic functions in C++?什么是 C++ 中可变参数函数的良好类型安全替代方案?
【发布时间】:2011-07-18 07:48:02
【问题描述】:

与此question 联合。我无法为以下看似基本的问题提出一个好的类型安全解决方案。我有一个音乐播放列表类,其中包含应该播放的歌曲列表。看起来很简单,只需制作队列中所有歌曲的 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);

虽然这很有效并且非常明确。打字非常繁琐,而且由于实际工作中的所有杂乱无章,它很容易出错。为了证明这一点,我实际上故意在上面的代码中添加了一个错误,这样的东西很容易制作,并且可能会通过代码审查而不受影响,而歌曲 4 从不在播放列表 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 不在列表的末尾,并且不是跨平台的,它将崩溃。在解决方案 B 中,您必须计算可能导致几个错误的参数数量。此外,没有一个解决方案是类型安全的,用户可以传入不相关的类型并在运行时等待崩溃。这似乎不是一个可持续的解决方案。然后我遇到了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;
}

这种语法不会让我们的小测试组感到困惑。但是,我非常担心如此严重地滥用操作员超载。所以我发布了上面的问题。我想看看在我们的测试组中比较专家的程序员是怎么想的。相当多的程序员不喜欢它,但它似乎比上述解决方案更好,因为它会在编译时而不是在运行时捕获大多数错误。然而,Tom 发布了一个有趣的反例,这会导致意外行为。

//lets combine differnt playlists
new_playlist.queue =    song1        //the first playlist
                      ,(song3,song4) //the second playlist //opps, I didn't add song 3!
                      , song5;

这让我对那个解决方案感到厌烦。那么您对更好的解决方案有什么想法吗?

附:以上代码均未编译,仅用于示例目的。

【问题讨论】:

  • 老实说,我看不出song1,song2,song3,song4,song5,song6 的单行添加比多行更能更好地发现错误?为什么人们会阅读一个而不是另一个?在我看来,如果你有不在列表/数组中的变量,那么你在调用代码中做错了,老实说,这不是你要解决的问题......
  • 您示例中的所有错误都与并发无关,只是编程不佳。
  • @littleadv 不幸的是,这是我必须处理的事情。由于该库的目标市场具有如此广泛的编程能力。他们中的一些人是该领域的专家,拥有 10 多年的经验,而其他人则参与编写他们的第一个 C++ 程序。库的整个设计试图使尽可能多的错误导致编译时错误或易于检测,只需浏览源文件即可。这是我无法应用此方法的唯一领域。
  • @RTS - 只是让我在答案中的观点更有说服力:假设客户是哑巴** - 确保 您的 代码是防犯规的。如果它需要保护 - 保护它你自己。至于两次添加同一首歌曲而错过另一首 - 你真的无能为力。在我看来,重复播放同一首歌是一个可行的用例,因此您不应将其视为错误。
  • 我太累了,不能写一个实质性的答案,所以我就把它留在这里..而不是使用逗号运算符,你应该真正坚持标准的 &lt;&lt; 运算符,它有很多先例。

标签: c++ type-safety variadic


【解决方案1】:

首先想到的问题是这是否是一个问题,以及这是否是一个值得解决的问题。由于您正在创建一个界面,您的客户将是用户code(不是人,code)。您的客户会在代码中硬编码播放列表多少次,而不是从文件中存储加载它们或根据用户选择构建它们?

考虑该功能的实际价值,并将其与他们如何使用您的库的影响进行比较,并考虑您的用户在更改界面时可能会遇到多少问题。

最简单的解决方案是接受迭代器来构造/重置您的列表,然后让用户处理问题。当然,他们可以按照您展示的方式构建自己的容器,但他们也可以使用 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

【讨论】:

  • 迭代器是最好的解决方案。
  • +1 用于 Boost.Assign,这是 Boost 的另一个部分,很少有人知道。
猜你喜欢
  • 2011-01-09
  • 1970-01-01
  • 1970-01-01
  • 2018-10-25
  • 1970-01-01
  • 2014-10-05
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多