C++中处理多个输入命令的正确方法是什么?

6

我有一个程序,可以从用户那里接收指令,并根据不同的指令进行不同的处理。

例如:

ADD_STUDENT ALEX 5.11 175
ADD_TEACHER MERY 5.4  120 70000
PRINT MERY 
REMOVE ALEX
PRINT TEACHER SALARY
PRINTALL 

因此,我需要检查每一行并查看输入由什么组成。
以下是我的代码,但我认为我误解了iss<<的工作方式。 有人能给我建议吗?并告诉我为什么我的代码没有按照我预期的那样工作呢?
string line;
while(getline(cin, line))
{
  //some initialization of string, float variable
  std::istringstream iss(line);
  if(iss >> command >> name >> height >> weight)
   ..examine the command is correct(ADD_STUDENT) and then do something..
  else if(iss >> command >> name >> height >> weight >> salary)
   ..examine the command is correct(ADD_TEACHER) and then do something...
  else if(iss >> command >> name)
   ..examine the command is correct(REMOVE) and then do somethin...
}

我认为 iss>> first >>second >> third 将在所有参数填充时返回 true,并在参数不足时返回 false。但显然我错了。

5个回答

10

你的问题描述得过于含糊不清。这总是促使我使用Boost Spirit提供一个过度膨胀的示例实现。

注意:请不要将此作为您的家庭作业提交。

查看Coliru上的演示,并使用以下示例输入:

ADD_STUDENT ALEX 5.11 175
ADD_STUDENT PUFF 6 7
ADD_STUDENT MAGIC 7 8
ADD_STUDENT DRAGON 8 9
ADD_TEACHER MERY 5.4  120 70000
PRINT MERY 
ADD_TEACHER DUPLO 5.4  120 140000
PRINTALL  10
REMOVE ALEX
PRINT  TEACHER SALARY
PRINT  MERY PUFF MAGIC DRAGON
REMOVE MERY PUFF MAGIC DRAGON
PRINT  TEACHER SALARY

完整代码:


更新:当包含make_visitor.hpp,如此展示的这里,您可以更优雅地编写访问者代码:

auto print_salary = [&] () 
{ 
    for(auto& p : names) 
        boost::apply_visitor(make_visitor(
                    [](Teacher const& v) { std::cout << "Teacher salary: " << v.salary << "\n"; },
                    [](Student const& v) {}), 
                p.second);
};

请查看适应的示例在 Coliru 上实时查看


#define BOOST_SPIRIT_USE_PHOENIX_V3
#include <boost/spirit/include/qi.hpp>
#include <boost/spirit/include/phoenix.hpp>

namespace qi = boost::spirit::qi;
namespace phx= boost::phoenix;

struct Person
{
    std::string name;
    double height, weight;
    friend std::ostream& operator<<(std::ostream& os, Person const& s) {
        return os << "Person { name:" << s.name << ", height:" << s.height << ", weight:" << s.weight << " }";
    }
};

struct Student : Person
{
    Student() = default;
    Student(std::string n, double h, double w) : Person {n,h,w} {}
};

struct Teacher : Person
{
    Teacher() = default;
    Teacher(std::string n, double h, double w, double s) : Person {n,h,w}, salary(s) {}
    double salary;
};

int main()
{
    std::stringstream ss;
    ss << std::cin.rdbuf();

    std::map<std::string, boost::variant<Student, Teacher> > names;

    using namespace qi;
    auto add_student  = phx::ref(names)[_1] = phx::construct<Student>(_1, _2, _3);
    auto add_teacher  = phx::ref(names)[_1] = phx::construct<Teacher>(_1, _2, _3, _4);
    auto remove       = phx::erase(phx::ref(names), _1);
    auto print_all    = [&] (int i) { for(auto& p : names) { std::cout << p.second << "\n"; if (--i==0) break; } };
    auto print_salary = [&] () 
    { 
        struct _ : boost::static_visitor<> {
            void operator()(Teacher const& v) const { std::cout << "Teacher salary: " << v.salary << "\n"; }
            void operator()(Student const& v) const { }
        } v_;
        for(auto& p : names) boost::apply_visitor(v_, p.second);
    };

    auto name_ = as_string[lexeme[+graph]];

    if (phrase_parse(begin(ss.str()), end(ss.str()), 
                (
                     ("ADD_STUDENT" >> name_ >> double_ >> double_)            [ add_student ]
                   | ("ADD_TEACHER" >> name_ >> double_ >> double_ >> double_) [ add_teacher ]
                   | (eps >> "PRINT" >> "TEACHER" >> "SALARY")                 [ print_salary ]
                   | ("PRINTALL" >> int_)      [ phx::bind(print_all, _1) ]
                   | ("PRINT"  >> +name_       [ std::cout << phx::ref(names)[_1] << std::endl ])
                   | ("REMOVE" >> +name_       [ remove ])
                ) % +eol,
                qi::blank))
    {
        std::cout << "Success";
    }
    else
    {
        std::cout << "Parse failure";
    }
}

输出:

Person { name:MERY, height:5.4, weight:120 }
Person { name:ALEX, height:5.11, weight:175 }
Person { name:DRAGON, height:8, weight:9 }
Person { name:DUPLO, height:5.4, weight:120 }
Person { name:MAGIC, height:7, weight:8 }
Person { name:MERY, height:5.4, weight:120 }
Person { name:PUFF, height:6, weight:7 }
Teacher salary: 140000
Teacher salary: 70000
Person { name:MERY, height:5.4, weight:120 }
Person { name:PUFF, height:6, weight:7 }
Person { name:MAGIC, height:7, weight:8 }
Person { name:DRAGON, height:8, weight:9 }
Teacher salary: 140000
Success

1
@kbok 谢谢。虽然我是开玩笑写这段代码的。但毫无疑问,它是最完整的答案。绝对没跑。就算是七里路也不在话下。 :) - sehe

7

做法如下:

iss >> command;
if (!iss)
    cout << "error: can not read command\n";
else if (command == "ADD_STUDENT")  
    iss >> name >> height >> weight;
else if (command == "ADD_TEACHER")  
    iss >> name >> height >> weight >> salary;
else if ...

6
std::string 不是非法的。第二个参数(C 字符串)将被转换为 std::string - Leonid Volnitsky
2
@LeonidVolnitsky,我认为c-string没有被转换,这个操作符只是有一个重载bool operator==(std::string lhs, const char* rhs) - Felix Glas
@user1701840 -- 是的,它是正确的,但在每个“else if”中都对“iss”值的状态进行了多余的检查。 - Leonid Volnitsky
1
通常情况下,当!iss触发时,它表示文件结束。确保它不会陷入无限循环。 - Mike DeSimone
@LeonidVolnitsky:Mike是正确的,文件末尾是流失败最常见的原因。但这甚至不必要证明他的观点。只要有任何一种方法会导致无限循环,程序就应该重新思考。但你是对的,没有无限循环。 - Ben Voigt
显示剩余8条评论

5

你的问题在于使用 >> 运算符会从流中读取并清除一个令牌。

if(iss >> command >> name >> height >> weight)

这段代码(上面的代码)试图从流中读取4个标记,对于每个成功的读取,它会清除从流中读取的数据。

else if(iss >> command >> name >> height >> weight >> salary)

当您看到这个(上面)时,意味着某个令牌无法被读取并转换为适当的类型,但很可能至少命令令牌已经从流中剥离。


2

好的,对于这个问题,现在赢得点赞已经太晚了,但你们启发了我的思考...

为了增强健壮性,您可以将解析分为两个阶段:第一阶段获取行,第二阶段获取一行并对其执行某些操作。

对于第一阶段,您可以使用getline

#include <string>
#include <sstream>

void ParseLines(std::istream& source)
{
    while(source)
    {
        // Get a line from the source.
        std::string inputLine;
        std::getline(source, inputLine);

        // Make a stream out of it.
        std::istringstream inputStream(inputLine);
        std::string command;
        inputStream >> command;
        if(inputStream) // Empty or bad line: skip
            HandleCommand(command, inputStream);
    }
}

第二阶段处理命令。它可能是直接的,像这样:

void HandleCommand(const std::string& command, std::istringstream& params)
{
    if(command == "ADD_STUDENT")
    {
        float someFloat;
        int someInt;
        params >> someFloat >> someInt;
        // add the student.
    }
    // etc.
}

但我并不觉得有什么丢脸的,我会实现一个工厂模式:

#include <map>

typedef void (*CommandHandler)(const std::string&, std::istringstream&);
typedef std::map<std::string, CommandHandler> CommandTable;

static CommandTable gCommands; // Yep. A global. Refactor however you see fit.

void HandleCommand(const std::string& command, std::istringstream& params)
{
    CommandTable::iterator handler = gCommands.find(command);
    if(handler == gCommands.end())
    {
        // Handle "command not found" error.
        return;
    }

    (*(handler->second))(command, params);
}

void AddStudent(const std::string& command, std::istringstream& params)
{
    float someFloat;
    int someInt;
    params >> someFloat >> someInt;
    // add the student.
}

// Other command handling functions here...

void RegisterCommands()
// Call this once prior to parsing anything,
// usually one of the first things in main().
{
    gCommands["ADD_STUDENT"] = &AddStudent;
    // ... other commands follow...
)

我没有测试过这些内容,但它应该大部分都在那里。请在评论中注意任何错误。

附言:这是极其低效的,比起一个经过良好设计的命令解析器,它将运行得更慢,然而,对于大多数工作来说它已经足够好了。


看起来比把所有东西都放在主函数里面要好!谢谢! - user1701840
1
哦呵呵,我的答案的代码行数**几乎和你的答案一样**。只是我的代码没有捷径:它实际上实现了命令和类型、存储和查找 :/ 哦,而且我发布得更晚了。(像你一样,我也是这种任务的痴迷者 :)) - sehe
1
有些人就是无法满足。我使用函数指针使答案简单化,并且故意限制为C++03或更早版本,因为我不知道这个人的编译器有多老。就像我说的那样,任何重构方式都可以,因为正确的重构取决于代码的其余部分如何组织,而没有发布任何内容。C++11提供了更好的选择。此外,我故意避免使用Boost Spirit,以免混淆可怜的OP的思维; 他们只是想使用istreamoperator >>。顺便说一句,感谢你的恶意点赞。 - Mike DeSimone
1
什么恶意?你的回答很糟糕。OP没有指定C++03,即使他这样做了,也有tr1::function和boost::function。至于全局变量,使用全局变量发布答案是不好的。你至少可以采用参数或其他方式。 - Puppy

1
您可以从技术上将整个输入行进行标记化,但这似乎离您的水平有些远。如果您确实想要进入其中,这里有一个很好的页面和教程 here,可以帮助您使用strtok()。

如果您不想采用这种方法,您可以逐个解析命令列表。假设您已经读取了一个名为“command”的字符串。

if (command == "ADD_STUDENT")
{
    int weight, height, otherfield;
    cout << ">" << flush;
    cin >> weight >> height >> otherfield;
    //do something, like add them to the database
}

那看起来是你最好的选择,虽然需要编写大量代码,但可能更容易实现。你可以深入了解并使用像这样的格式字符串:

scanf("%s, %s %d, %f", lastname, firstname, age, height);

这样,输入看起来像这样:

This way, the input would look like this:

ADD_STUDENT Doe, John 30, 5.6

你不需要使用 scanfstrtok 来对行进行分词。请参考 https://dev59.com/cHVD5IYBdhLWcg3wNIvc。 - Mike DeSimone
@Mike,这真的取决于你使用的字符串类型。我从不使用std::string,所以<T>.split()对我无效。 - phyrrus9

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接