Strong types in C++

Ábel Sinkovics

References

Student representation

struct student {
  std::string name;
  std::string class_teacher;
};

int main() {
  // https://xkcd.com/327
  student bobby_tables{
    "Robert'); DROP TABLE Students;--",
    "John Thomas"
  };
}
          

Student representation

class student {
public:
  student(std::string name, std::string class_teacher) :
    name_(std::move(name)), class_teacher_(std::move(class_teacher))
  {
    if (!valid_name(name_)) { throw invalid_name(name_); }
    if (!valid_name(class_teacher_))
      { throw invalid_name(class_teacher_); }
  }

  const std::string& name() const;
  const std::string& class_teacher() const;
private: 
  std::string name_;
  std::string class_teacher_;
};

If you can not or do not want to throw exceptions, you can use factory methods instead of throwing constructors.

Search for students

$ ./display_student_details
Please provide student's name: Robert'); DROP TABLE Students;--

  Robert'); DROP TABLE Students;-- not foundInvalid name: Robert'); DROP TABLE Students;--

Search for students

int main()
{
  std::vector<student> students = load_students();
  std::string name = read_name("Please provide student's name");
  if (valid_name(name)) { /* ... */ }
  else { std::cerr << "Invalid name: " << name << "\n"; }
  // ...or can we trust read_name?
  // ...should we assert?
}
          

Strong types

class person_name {
public:
  // invariant: valid_name(value_);

  explicit person_name(std::string val) : value_(std::move(val))
  { if (!valid_name(value_)) { throw invalid_name(value_); } }

  explicit operator std::string() const { return value_; }

private:
  std::string value_;
};

person_name

struct student {
  person_name                    name;
  person_name                    class_teacher;
};

int main()
{
  std::vector<student>   students = load_students();
  person_name
    name =         read_name("Please provide student's name");
  /* ... */
}
          

More specific?

struct student {
  student_name                   name;
  teacher_name                   class_teacher;
};

int main()
{
  std::vector<student>   students = load_students();
  student_name
    name = read_student_name("Please provide student's name");
  /* ... */
}
          

Most/too specific?

struct student {
  student_name_member_t          name;
  student_class_teacher_member_t class_teacher;
};

int main()
{
  load_students_return_t students = load_students();
  read_student_name_return_t
    name = read_student_name("Please provide student's name");
  /* ... */
}

// Implicit conversion:
// read_student_name_return_t -> student_name_member_t
          

Least specific?






int main()
{
  std::any               students = load_students();
  std::any
    name = read_student_name("Please provide student's name");
  /* ... */
}
          

How specific?

  • How specific is the best?
  • Use our intuition?
  • std::string was initially intuitive

Problem vs implementation domain

  • Problem domain: person_name, student_name, teacher_name
  • Implementation domain: std::string
  • Implementation domain: char[]
  • Implementation domain: bit sequence
  • ...

When to use specific types?

  • What should be modeled as a std::string?
  • When something is a std::string, wierd values are valid, eg. "\a\b".
  • What should be modeled as an int? The set of values is platform specific.
  • Other basic types: long int, double, etc.

How about empty names?

  • Empty string often means no value
  • Is it a valid name?
  • Based on conventions (like pointers):
    • When to expect empty/missing value?
    • People often forget to check for it
    • The fact that there might be no value is hidden

Alternate for empty names

  • Reject empty names and use std::optional<person_name> when needed
  • person_name is no longer default-constructible
  • std::vector<person_name>?

Legacy code

  • Identify new type (eg. person_name)
  • Identify one location, where it should be used. (eg. name member of student type)
  • Let the compiler lead further updates
  • Limited time: explicit casting from/to the underlying type can limit the scope of changes done at once.

Evolution of a type

struct student {
  person_name name;
  person_name class_teacher;
};

int main() {
  student bobby_tables{
    person_name("Bobby Tables"),
    person_name("John Thomas")
  };
}
          
  • English order: John Thomas
  • Hungarian order: Thomas John
  • What order should we expect?

Evolution of a type

enum class name_prefix { /* ... */ };

class person_name {
public:






// ...

private:
  std::string prefix_;
  std::vector<std::string> given_names_;
  std::string family_name_;  name_prefix prefix_;
  std::vector<given_name > given_names_;
  family_name family_name_;


};
          

Redundant argument names

enum class name_prefix { /* ... */ };

class person_name {
public:
  person_name(
    name_prefix prefix,
    given_name the_given_name,
    family_name the_family_name
  );

// ...

private:
  name_prefix prefix_;
  std::vector<given_name > given_names_;
  family_name family_name_;
};
          

...not always redundant

void copy_file(
  const std::filesystem::path&,
  const std::filesystem::path&  const readable_path        &,
  const writeable_path       &

);

// copy_file("source", "destination") ?
// copy_file("destination", "source") ?
          

Lifting the API

void update_db(const given_name& requestor);
{
  if (!entitled_to_updated_db(requestor))
  {
    throw error(requestor + " is not entitled to updated database");
  }
  // ...
}

int main()
{
  given_name john("John");
  update_db(john + "athan");
}
          

What should given_name + const char[] do?

Lifting the API

  • No universally good answer
  • Not everything needs lifting
    • meter type wrapping double
    • Should meter / meter be valid?
    • Should meter / double be valid?

Impact on development

  • Mandates understanding the problem domain
  • No more "just store/pass around the string provided"
  • Highlights design overlooks

Strong type

Strong typedef

Concepts

  • Strong types
    • generic → specific.
    • Eg: std::stringgiven_name
  • Concepts
    • generic ← specific.
    • Eg. CharacterSequence<T>given_name

Contracts

  • A number of preconditions can be captured by invariant of stronger types.
  • A number of postconditions can be captured by invariant of strong types.

Library support

Performance

Any guidelines?

  • Red flag: using basic type.
  • Red flag: function argument name adds extra information.
  • Are these good guidelines?

Q & A