最优雅且不易出错的方法来检查ctor参数

时间:2013-10-18 17:24:08

标签: c++

给出一个person类的以下简单构造函数声明:

Person(const string& _firstName, const string& _lastName, const string& _id);

哪种方式被认为是优雅且不易出错以确保给定参数有效? 假设我想确保PERSON类型的对象包含空字符串,即使只有一个给定的参数无效。 我提出了这个解决方案:

Person::Person(const string& _firstName, const string& _lastName, const string& _id) {
    if (!isValidName(_firstName) || !isValidName(_lastName)) {
                        firstName = ""; lastName = ""; id = "";
    throw InvalidNameException();
}
if (!isValidID(_id)) {
    firstName = ""; lastName = ""; id = "";
    throw InvalidIDException();
}
firstName = _firstName;
lastName = _lastName;
id = _id;
} 

现在ctor对我来说太过臃肿了,我想过写一个init方法,但我真的不喜欢那个解决方案。

很想听听一些建议。

5 个答案:

答案 0 :(得分:3)

我建议在初始化列表中初始化您的成员变量,然后检查它们是否有效。如果他们不这样做,就抛出异常。

Person::Person(const string& _firstName, const string& _lastName, const string& _id) : 
    firstName( _firstName ),
    lastName( _lastName ),
    id( _id )
{
    if (!isValidName( firstName ) || !isValidName( lastName ) || !isValidID( id ) ) {
        throw InvalidNameException();
}

在进入构造函数体之前,会初始化您的成员变量。问题中的代码会将成员变量初始化为空,然后在将它们重新分配时再次初始化它们。

通过抛出异常,无论如何都不认为该对象是被构造的,因此您不需要清除"清除"成员变量。在抛出异常之前成功构造的任何成员变量都将调用其析构函数。请注意,如果您的对象抛出异常,则抛出它将不会调用其析构函数(因为它从未完全创建)。

答案 1 :(得分:3)

为什么不包含isValidNameisValidId函数 抛出或返回字符串的函数:

std::string
checkedName( std::string const& name )
{
    if ( !isValidName( name ) ) {
        throw InvalidNameException();
    }
    return name;
}

std::string
checkedId( std::string const& id )
{
    if ( !isValidID( id ) ) {
        throw InvalidIDException();
    }
    return id;
}

然后:

Person::Person( std::string const& firstName,
                std::string const& lastName,
                std::string const& id )
    : myFirstName( checkedName( firstName ) )
    , myLastName( checkedName( lastName ) )
    , myId( checkedId( id ) )
{
}

或者(但在我看来可读性较差),你可以使用 三元运算符:

Person::Person( std::string const& firstName,
                std::string const& lastName,
                std::string const& id )
    : myFirstName( isValidName( firstName )
                    ? firstName
                    : throw InvalidNameException() )
    , myLastName( isValidName( lastName )
                    ? lastName
                    : throw InvalidNameException() )
    , myId( isValidID( id )
            ? id
            : throw InvalidIDException() )
{
}

无论哪种方式,您都不要进入构造函数的主体 除非字符串有效。

答案 2 :(得分:0)

那几乎没有臃肿。这只是几行。但是在构造函数中执行错误检查总是有问题的。

最好的方法可能就是抛出异常,并让调用者决定如何处理错误。

我确实无法想象为什么在确定参数无效后必须将_firstName_lastName设置为空字符串。

Person::Person(const string& _firstName, const string& _lastName, const string& _id) {
    if (!isValidName(_firstName) || !isValidName(_lastName))
        throw InvalidNameException();
    if (!isValidID(_id))
        throw InvalidIDException();
    firstName = _firstName;
    lastName = _lastName;
    id = _id;
}

这根本不是臃肿。

答案 3 :(得分:0)

如果从构造函数中抛出,则认为您的对象从未构造过。在这种情况下,没有必要将值设置为空字符串,因为在从构造函数中抛出异常后,以某种方式尝试访问成员时,它将是未定义的行为。

Person::Person(const string& _firstName, const string& _lastName, const string& _id)
    : firstName(_firstName), lastName(_lastName), id(_id) {
    if (!isValidName(firstName) || !isValidName(lastName)) throw InvalidNameException();
    if (!isValidID(_id)) throw InvalidIDException();
}

答案 4 :(得分:0)

我认为您需要将您的疑虑分开。

  • 如果first_name没有last_name
  • ,是否有意义?
  • 在没有first_name的情况下拥有last_nameid是否有意义?
  • 有无效first_namelast_nameid是否有意义?

我相信所有问题的答案都是“不”。出于这个原因,我认为这三个实体属于一个名为Identifier(或类似)的抽象概念。情况就是这样,因为您需要提供的检查和保证并非易事。

该课程可能如下所示:

class Identifier {

 public:

  static void ValidateName(const std::string& name) {
    if(name.size() <= 0) {
      std::stringstream ss;
      ss<<"Invalid name: "<<name<<" (expected non-empty string)";
      throw std::invalid_argument(ss.str());
    }
  }

  static void ValidateId(const std::string& id) {
    if(id.size() != 10) {
      std::stringstream ss;
      ss<<"Invalid id: "<<id<<" (expected string of length 10)";      
      throw std::invalid_argument(ss.str());
    }
  }

  Identifier(const std::string& first, const std::string& last, const std::string& id)
      : m_first(first),
        m_last(last),
        m_id(id) {
    Identifier::ValidateName(m_first);
    Identifier::ValidateName(m_last);
    Identifier::ValidateId(m_id);
  }

  const std::string& first_name() const {
    return m_first;
  }

  const std::string& last_name() const {
    return m_last;
  }

  const std::string& id() const {
    return m_id;
  }

 private:
  std::string m_first;
  std::string m_last;
  std::string m_id;
};

考虑到类的设计方式,不可能创建无效的Identifier:要么通过所有测试,要么首先创建Identifier

此外,请注意,Identifier创建后无法使其无效;这保证了如果Identifier的实例存在,它将在整个存在期间有效。

鉴于该保证,您现在可以通过构造函数创建Person实例,该构造函数采用Identifier;构造函数永远不会抛出,也不需要进行任何检查。 Person类可能如下所示:

class Person {
 public:
  Person(const Identifier& identifier) noexcept(true)
      : m_identifier(identifier) { }

  const std::string& first_name() const {
    return m_identifier.first_name();
  }

  const std::string& last_name() const {
    return m_identifier.last_name();
  }

  const std::string& id() const {
    return m_identifier.id();
  }

 private:
  Identifier m_identifier;
};

以这种方式分离您的顾虑:

  • 您可以使用进一步的检查或字段来扩充Identifier课程,而无需触及Person课程:您的课程可以独立发展
  • 您可以在其他地方使用Identifier,而无需重复检查。例如:您可以创建Identifier并将它们引入数据库。

最终代码是:

#include<iostream>
#include<sstream>
#include<stdexcept>

class Identifier {

 public:

  static void ValidateName(const std::string& name) {
    if(name.size() <= 0) {
      std::stringstream ss;
      ss<<"Invalid name: "<<name<<" (expected non-empty string)";
      throw std::invalid_argument(ss.str());
    }
  }

  static void ValidateId(const std::string& id) {
    if(id.size() != 10) {
      std::stringstream ss;
      ss<<"Invalid id: "<<id<<" (expected string of length 10)";      
      throw std::invalid_argument(ss.str());
    }
  }

  Identifier(const std::string& first, const std::string& last, const std::string& id)
      : m_first(first),
        m_last(last),
        m_id(id) {
    Identifier::ValidateName(m_first);
    Identifier::ValidateName(m_last);
    Identifier::ValidateId(m_id);
  }

  const std::string& first_name() const {
    return m_first;
  }

  const std::string& last_name() const {
    return m_last;
  }

  const std::string& id() const {
    return m_id;
  }

 private:
  std::string m_first;
  std::string m_last;
  std::string m_id;
};


class Person {
 public:
  Person(const Identifier& identifier) noexcept(true)
      : m_identifier(identifier) { }

  const std::string& first_name() const {
    return m_identifier.first_name();
  }

  const std::string& last_name() const {
    return m_identifier.last_name();
  }

  const std::string& id() const {
    return m_identifier.id();
  }

 private:
  Identifier m_identifier;
};


int main() {
  Identifier id("John", "Doe", "1234567890");

  Person p(id); // cannot throw because id has already been
                // constructed

  std::cout<<p.last_name()<<", "<<p.first_name()<<" Id: "<<p.id()<<std::endl;

  try {
    Identifier id2("Sue", "Smith", "126789");
    Person p2(id2);
    std::cout<<p2.first_name()<<std::endl;
  } catch(const std::exception &e) {
    std::cout<<e.what()<<std::endl;
  }

  try {
    Identifier id3("", "Smith", "126789");
    Person p3(id3);
    std::cout<<p3.first_name()<<std::endl;
  } catch(const std::exception &e) {
    std::cout<<e.what()<<std::endl;
  }


  return 0;
}

可以使用以下命令(OS X 10.7.4上的GCC 4.8.1)编译

g++ validated_names.cpp -std=c++11 -Wall -Wextra

并产生以下输出:

./a.out 
Doe, John Id: 1234567890
Invalid id: 126789 (expected string of length 10)
Invalid name:  (expected non-empty string)