C++11 Via TFL (Test-Focused Learning): Uniform Initialization

by Jeff Langr

January 16, 2013

I’ve been working with the new features of C++11 for many months as I write a book on TDD in C++. The updates to the language make for a far-more satisfying experience, particularly in terms of helping me write clean code and tests.

I haven’t written any Java code for over a half-year, and I don’t miss it one bit (I do miss the IDEs a bit, although I’m enjoying working in vim again, particularly with a few new tips picked up from Use Vim Like a Pro and Practical Vim). New language features such as lambdas and type inferencing represent a leap-frogging that shine a little shame on Oracle’s efforts.

source: Naval History and Heritage Command

Image source: Naval History & Heritage Command

Over a series of upcoming blog entries, I will be demonstrating many of the new language features in C++ via test-focused learning (TFL). This entry: uniform initialization, the new scheme for universally-consistent initialization that also simplifies the effort to initialize collections, arrays, and POD types.

One goal of this blog series is to see how well the tests can communicate for themselves. Prepare for a lot of test code (presume they all pass, unless otherwise noted) and little blather. Please feel free to critique by posting comments; there’s always room for improvement around the clarity of tests (particularly regarding naming strategy). Note that TFL and TDD have slightly different goals; accordingly, I’ve relaxed some of the TDD conventions I might otherwise follow (such as one assert per test).

The Basics

Note: Your compiler may not be fully C++11-compliant. The examples shown here were built (and tested) under gcc 4.7.2 under Ubuntu. The unit testing tool is Google Mock (which supports the Hamcrest-like matchers used here, and includes Google Test).

    TEST(BraceInitialization, SupportsNumericTypes) {
       int x{42};
       ASSERT_THAT(x, Eq(42));
    
       double y{12.2};
       ASSERT_THAT(y, DoubleEq(12.2));
    }
    
    TEST(BraceInitialization, SupportsStrings) {
       string s{"Jeff"};
       ASSERT_THAT(s, Eq("Jeff"));
    }
    
    TEST(BraceInitialization, SupportsCollectionTypes) {
       vector names {"alpha", "beta", "gamma" };
       ASSERT_THAT(names, ElementsAre("alpha", "beta", "gamma"));
    }
    
    TEST(BraceInitialization, SupportsArrays) {
       int xs[] {1, 1, 2, 3, 5, 8};
       ASSERT_THAT(xs, ElementsAre(1, 1, 2, 3, 5, 8));
    }

Those tests are simple enough. Maps are supported too:

    TEST(BraceInitialization, SupportsMaps) {
       map heights {
          {"Jeff", 176}, {"Mark", 185}
       };
    
       ASSERT_THAT(heights["Jeff"], Eq(176));
       ASSERT_THAT(heights["Mark"], Eq(185));
    }

Explicit initialization of collections isn’t nearly as prevalent in production code as it is in tests. I’m tackling uniform initialization first because I’m so much happier with my resulting tests. The ability to create an initialized collection in a single line is far more expressive than the cluttered, old-school way.

    TEST(OldSchoolCollectionInitialization, SignificantlyCluttersTests) {
       vector names;
    
       names.push_back("alpha");
       names.push_back("beta");
       names.push_back("gamma");
    
       ASSERT_THAT(names, ElementsAre("alpha", "beta", "gamma"));
    }

No Redundant Type Specification!

Uniform initialization eliminates the need to redundantly specify type information when you need to pass lists.

    TEST(BraceInitialization, CanBeUsedOnConstructionInitializationList) {
       struct ReportCard {
          string grades[5];
          ReportCard() : grades{"A", "B", "C", "D", "F"} {}
       } card;
    
       ASSERT_THAT(card.grades, ElementsAre("A", "B", "C", "D", "F"));
    }
    
    TEST(BraceInitialization, CanBeUsedForReturnValues) {
       struct ReportCard {
          vector gradesForAllClasses() {
             string science{"A"};
             string math{"B"};
             string english{"B"};
             string history{"A"};
             return {science, math, english, history};
          }
       } card;
    
       ASSERT_THAT(card.gradesForAllClasses(), ElementsAre("A", "B", "B", "A"));
    }
    
    TEST(BraceInitialization, CanBeUsedForArguments) {
       struct ReportCard {
          vector subjects_;
    
          void addSubjects(vector subjects) {
             subjects_ = subjects;
          }
       } card;
    
       card.addSubjects({"social studies", "art"});
    
       ASSERT_THAT(card.subjects_, ElementsAre("social studies", "art"));
    }

Direct Class Member Initialization

Joyfully (it’s about time), C++ supports directly initializing at the member level:

    TEST(BraceInitialization, CanBeUsedToDirectlyInitializeMemberVariables) {
       struct ReportCard {
          string grades[5] {"A", "B", "C", "D", "F"};
       } card;
    
       ASSERT_THAT(card.grades, ElementsAre("A", "B", "C", "D", "F"));
    }

Class member initialization essentially translates to the corresponding mem-init. Be careful if you have both:

    TEST(MemInit, OverridesMemberVariableInitialization) {
       struct ReportCard {
          string schoolName{"Trailblazer Elementary"};
          ReportCard() : schoolName{"Chipeta Elementary"} {}
       } card;
    
       ASSERT_THAT(card.schoolName, Eq("Chipeta Elementary"));
    }

Temporary Type Name

    TEST(BraceInitialization, EliminatesNeedToSpecifyTempTypeName) {
       struct StudentScore {
          StudentScore(string name, int score) 
             : name_(name), score_(score) {}
          string name_;
          int score_;
       };
       struct ReportCard {
          vector scores_;
          void AddStudentScore(StudentScore score) {
             scores_.push_back(score);
          }
       } card;
    
       // old school: cardAddStudentScore(StudentScore("Jane", 93));
       card.AddStudentScore({"Jane", 93}); 
    
       auto studentScore = card.scores_[0];
       ASSERT_THAT(studentScore.name_, Eq("Jane"));
       ASSERT_THAT(studentScore.score_, Eq(93));
    }

Be careful that use of this feature does not diminish readability.

Defaults

    TEST(BraceInitialization, WillDefaultUnspecifiedElements) {
       int x{};
       ASSERT_THAT(x, Eq(0));
    
       double y{};
       ASSERT_THAT(y, Eq(0.0));  
    
       bool z{};
       ASSERT_THAT(z, Eq(false));
    
       string s{};
       ASSERT_THAT(s, Eq(""));
    }
    
    TEST(BraceInitialization, WillDefaultUnspecifiedArrayElements) {
       int x[3]{};
       ASSERT_THAT(x, ElementsAre(0, 0, 0));
    
       int y[3]{100, 101};
       ASSERT_THAT(y, ElementsAre(100, 101, 0));
    }
    
    TEST(BraceInitialization, UsesDefaultConstructorToDeriveDefaultValue) {
       struct ReportCard {
          string school_;
          ReportCard() : school_("Trailblazer") {}
          ReportCard(string school) : school_(school) {}
       };
    
       ReportCard card{};
    
       ASSERT_THAT(card.school_, Eq("Trailblazer"));
    }

Odds & Ends

    TEST(BraceInitialization, CanIncludeEqualsSign) {
       int i = {99};
       ASSERT_THAT(i, Eq(99));
    }

… but why bother?

It’s always nice when a new language feature makes it a little harder to make the dumb mistakes that we all tend to make from time to time (and sometimes, such dumb mistakes are the most devastating).

    TEST(BraceInitialization, AvoidsNarrowingConversionProblem) {
       int badPi = 3.1415927;
       ASSERT_THAT(badPi, Eq(3));
    
       int pi{3.1415927}; // emits warning by default
    //   ASSERT_THAT(pi, Eq(3.1415927));
    }

Running the AvoidsNarrowingConversionProblem test results in the following warning:

    warning: narrowing conversion of ‘3.1415926999999999e+0’ from ‘double’ to ‘int’ inside { } [-Wnarrowing]

Recommendation: use the gcc compiler switch:

    -Werror=narrowing

…which will instead cause compilation to fail.

Use With Auto

    TEST(BraceInitialization, IsProbablyNotWhatYouWantWhenUsingAuto) {
       auto x{9};
       ASSERT_THAT(x, A>());
       // in other words, the following assignment passes compilation. Thus x is *not* an int.
       const initializer_list y = x;
    }

The Most Vexing Parse?

It’s C++. That means there are always tricky bits to avoid.

    TEST(BraceInitialization, AvoidsTheMostVexingParse) {
       struct IsbnService {
          IsbnService() {}
          string address_{"http://example.com"};
       };
    
       struct Library {
          IsbnService service_;
          Library(IsbnService service) : service_{service} {}
          string Lookup(const string& isbn) { return "book name"; }
       };
    
       Library library(IsbnService()); // declares a function(!)
    //   auto name = library.Lookup("123"); // does not compile
    
       Library libraryWithBraceInit{IsbnService()};
       auto name = libraryWithBraceInit.Lookup("123"); 
    
       ASSERT_THAT(name, Eq("book name"));
    }

All the old forms of initialization in C++ will still work. Your best bet, though, is to take advantage of uniform initialization and use it at every opportunity. (I’m still habituating, so you’ll see occasional old-school initialization in my code.)

Pingback: C++11: Sum Across a Collection of Objects Using a Lambda or a Range-Based For Loop

Pingback: C++11 Via TFL (Test-Focused Learning): Uniform Initialization | langrsoft.com : ???????

Comments

Dale Keener January 16, 2013 at 6:47 am

Jeff,

Great to hear you’re doing a book on TDD in C++. It’s sorely needed.

Is there a target date? Who’s publisher?

Are you providing any early previews for feedback?

Thanks,

Dale Keener


Jeff Langr January 16, 2013 at 8:21 am

Hi Dale,

I’m hoping to see the book out around early summer. The book will be published by PragProg.

If you’re interested in helping with reviews, please send me an email (jeff@langrsoft.com).

Regards,
Jeff


Randy Coulman January 16, 2013 at 10:28 pm

Jeff, these are great! Looking forward to more.

One minor nit:

  TEST(BraceInitialization, WillDefaultUnspecifiedElements) {
  //....
     double y{};
     ASSERT_THAT(y, Eq(0L));
  // ...
  }

Shouldn’t that be 0.0 instead of oL? oL is of type long, I believe.


Jeff Langr January 17, 2013 at 8:52 am

Thansk Randy, good spot & problem fixed in the post.


Share your comment

Jeff Langr

About the Author

Jeff Langr has been building software for 40 years and writing about it heavily for 20. You can find out more about Jeff, learn from the many helpful articles and books he's written, or read one of his 1000+ combined blog (including Agile in a Flash) and public posts.