Authoring C++ Content
This article is meant for kata authors and translators who would like to create new content in C++. It attempts to explain how to create and organize things in a way conforming to authoring guidelines, shows the most common pitfalls and how to avoid them.
This article is not a standalone tutorial on creating kata or translations. It's meant to be a complementary, C++-specific part of a more general set of HOWTOs and guidelines related to content authoring. If you are going to create a C++ translation, or a new C++ kata from scratch, please make yourself familiar with the aforementioned documents related to authoring in general first.
#
General infoAny technical information related to the C++ setup on Codewars can be found on the C++ reference page (language versions, available libraries, and setup of the code runner).
#
DescriptionC++-specific code can be inserted with the cpp
flag in sequential code blocks:
```cpp
...your code here...
```
C++-specific paragraphs can be inserted with language conditional rendering:
~~~if:cpp
...text visible only for C++ description...
~~~
~~~if-not:cpp
...text not visible in C++ description...
~~~
#
Tasks and RequirementsSome concepts don't always translate well to or from C++. C++ allows for a variety of paradigms and techniques, many of which are not necessarily suited to other languages. For this reason, certain constructs should be avoided and some kata just should not be translated to C++.
- C++ is statically typed, so any task that depends on dynamic typing can be difficult to translate into C++, and attempts to force a C++ kata to reflect a dynamically typed interface can lead to ideas that enforce a really bad design.
- Many features of C++ do not have direct equivalents in other popular languages: mixture of allowed paradigms, template meta-programming, unmanaged memory backed by RAII, native access to the platform and runtime, and many others. C++ kata which rely on them can be difficult to translate to other languages.
#
Kata snippets and templateDue to the complicated compilation model of C++ code, paired with the fact that Codewars strictly enforces the names, amount, and meaning of source files, the code runner uses a template to concatenate all kata snippets into a single translation unit. This fact has various consequences on the code, some positive, and some negative: namespace pollution, symbols introduced by one snippet being visible in other snippets, etc. However, in the majority of cases, it does not affect kata in any way, and whenever possible, this behavior should be treated as an implementation detail of the C++ code runner. Kata snippets should be treated as separate source files, if possible.
One consequence of using a template is that the signature of a solution function can be modified by the user in a way which can affect tests. Make sure that tests are not possible to work around by users tampering with the prototype of the solution function.
Another issue caused by the use of the template is excessive namespace pollution. See paragraph on using
directives` for more details.
#
Coding#
Code styleC++ programmers have many sets of naming conventions or code style guides. Some of them can be found for example in ISO C++ FAQ, or Google C++ Style Guide. Codewars does not strictly enforce any of them, just use whatever code style you like, but when you do, use it consistently.
There are a few C++ language guidelines which are violated by kata authors and translators particularly often:
- Arguments which are not cheap to copy should be passed by const reference, and not by value.
- Appropriate containers should be used, especially when used as input arguments or return values:
- C-style raw arrays and pointers to arrays need to be avoided
std::array
,std::pair
, orstd::tuple
should be used for data of known size. Avoid representing 2D points or pairs withstd::vector
.std::vector
is not the only available option, and can be replaced by another container (for example,std::set
), a set of iterators, a range, or any other construct suitable for the task.std::string
orstd::string_view
should be used instead of C-strings
#
Header filesC++ authors often forget to include required header files, or just leave them out deliberately because "it works" even when some files are not included. It happens mostly due to the following reasons:
- C++ setup used by the Codewars runner uses a somewhat specific way to prepare and run kata snippets (see Kata snippets and template). The structure of concatenated files which the runner compiles causes some header files to always be included, or makes some files included in one kata snippet automatically available to some other kata snippet. However, this behavior should not be relied upon if not necessary.
- Some header files include other header files indirectly; for example, if file
foo.h
contains line#include <bar.h>
, that might appear to make the explicit include forbar.h
unnecessary. This is not true though, because the filefoo.h
might change one day, or might depend on some compiler settings or command line options, and after some changes to the configuration of the C++ runner, the filebar.h
might be not included there anymore.
Every snippet should include all header files required by the code it contains.
#
Compilation warningsCompiler options related to warnings used by the C++ runner are somewhat strict and require some discipline to get the code to compile cleanly. -Wall
and -Wextra
may cause numerous warnings and some of them are very pedantic. However, code of C++ kata should still compile cleanly, without any warnings logged to the console. Even when a warning does not cause any problem with tests, users get distracted by them and blame them for failing tests.
#
Avoiding CSometimes authors consider C++ just "C, but with classes". While C and C++ are compatible in many ways, that kind of perception is wrong: incorrect use of C features in C++ code leads to bad code at best, or to undefined behavior and difficult to diagnose errors in more extreme cases. Features and idioms from C language should be replaced with their equivalents from modern C++:
- Proper C++ data types should be used instead of their C "equivalents", especially when used as elements of a solution interface (input parameters and return value). C-strings should be replaced with
std::string
(orstd::string_view
), raw C-style arrays should be replaced withstd::array
,std::vector
, or other containers, smart pointers should be considered instead of raw pointers, etc. - C++ header files should not be confused with their C equivalents. For example,
cmath
orcctype
should be used instead ofmath.h
orctype.h
. - C features must not be used where they do not have well-defined behavior in C++. For example, memory management generally should not be done with
malloc
/free
or similar, and VLAs should be replaced withstd::vector
.
using
directives#
To avoid problems with namespace pollution, C++ snippets should not contain using
directives (particularly using namespace std;
) anywhere in the global namespace. Incorrect use of using
can sometimes cause compilation problems that are difficult to diagnose. For example, using namespace std;
placed incorrectly in the "Complete solution" or "Initial solution" snippet can result in failed compilation for some users. To minimize such risks, authors should stick to following guidelines:
using
directives should not be present in the global scope of "Preloaded", "Complete solution", and "Initial solution" snippets. They are heavily discouraged in the global scope of "Sample tests" and "Submission tests" snippets. This rule applies not only tousing namespace std;
, but also to other namespaces and namespace-qualified names.using
directives are perfectly fine inside the scope of an "Initial solution" function, test helpers, orIt
blocks.
#
TestsC++ kata use the modified version of the Igloo testing framework, along with a modified version of the Snowhouse assertion library. Codewars modified both libraries to adapt them to the code runner environment and make them more useful for kata authors.
#
IglooGeneral structure of tests created with Igloo can be presented as:
Describe(TestGroup_1) {
It(TestCase_1) {
//...
}
It(TestCase_2) {
//...
}
};
Describe(TestGroup_2) {
It(TestCase_1) {
//...
}
};
Describe
blocks are expanded into C++ struct
s, and they can contain anything a struct
can, in particular public
and private
access modifiers, member fields and functions, etc. Describe
blocks can also be nested, with one caveat: while nested Describe
sections are correctly compiled and registered for execution, the test output panel does not report them hierarchically; they are all flattened into a single-level test report.
#
SnowhouseSnowhouse
provides the function Assert::That
, which can accept either a constraint which has to be fulfilled by an asserted value (Assert::That(actual, Equals(expected))
), or a fluent expression accepting actual value as input and returning a boolean value indicating success or failure (Assert::That(actual, Is().Equal(expected))
). Both types of assertions are equivalent to each other and authors can choose whichever suits them better. For clarity, this document uses only constraints as examples, but each of them have corresponding expressions. The most useful ones are:
Equals
EqualsWithDelta
EqualsContainer
#
Custom assertion messagesA drawback of the original version of the Snowhouse assertion library is that neither its AssertThat
macro nor Assert::That
function accept a custom assertion message which could be used by authors to provide detailed information on the cause of failure. To support authors with possibility to provide useful feedback, Codewars provides a set of overloads for Assert::That
, which accept an additional message supplier:
template<typename ActualType, typename ConstraintListType, typename MessageSupplierType>
static void That(const ActualType& actual, ExpressionBuilder<ConstraintListType> expression, const MessageSupplierType& message_supplier, const char* file_name = "", int line_number = 0);
template<typename ConstraintListType, typename MessageSupplierType>
static void That(const char* actual, ExpressionBuilder<ConstraintListType> expression, const MessageSupplierType& message_supplier, const char* file_name = "", int line_number = 0)
template<typename ActualType, typename ExpressionType, typename MessageSupplierType>
static void That(const ActualType& actual, const ExpressionType& expression, const MessageSupplierType& message_supplier, const char* file_name = "", int line_number = 0);
template<typename ExpressionType, typename MessageSupplierType>
static void That(const char* actual, const ExpressionType& expression, const MessageSupplierType& message_supplier, const char* file_name = "", int line_number = 0);
message_supplier
is a callable compatible with a function accepting no arguments and returning std::string
. It can be a function, a functor, a lambda expression, or an instance of any type supporting above requirements.
For more details and examples of custom assertion messages, see [Example test suite](#example-test-suite)
below, or visit the [Snowhouse reference](/languages/cpp/snowhouse/)
page.
#
StringizersIn its default configuration, Snowhouse can produce confusing assertion messages when expected
and actual
values are of a type it cannot stringify:
does_not_pretty_print_type_without_stringizer
Expected: equal to [ [unsupported type], [unsupported type] ]
Actual: [ [unsupported type], [unsupported type] ]
To rectify this issue in your tests, you can make such types suitable for stringification in a way described in the dedicated documentation article on custom Snowhouse stringizers.
#
Random utilitiesUntil C++11, the most common way of producing random values was the rand
function. However, it has a set of problems: it needs to be properly seeded, and it is difficult to produce values outside of 0...RAND_MAX
range. Since C++11, the standard library offers a set of functionalities which are designed to produce random values of different types, from various ranges, and with better distribution when compared to rand
. Unfortunately, the API presented in the random
header seems to be confusing and difficult to use and accompanied by a few misconceptions, and authors either use it incorrectly or resort to (not) good, old rand
. However, working with random
turns out to be not that difficult:
// use random_device only as a seeder
std::random_device seeder;
// create one PRNG which will be used to pick values
// from (potentially many) distributions
std::mt19937 engine{ seeder() };
// a set of distributions for every type or range you are going to need in your tests
std::uniform_int_distribution< int> rand_number { 1, 100 };
std::uniform_int_distribution< size_t> rand_length { 20, 100 };
std::uniform_real_distribution<double> rand_coefficient{ -1, 1 };
std::uniform_int_distribution< char> rand_letter { 'a', 'z' };
std::uniform_int_distribution< char> rand_bool { 0, 1 };
// etc.
// for convenience, distributions can be bound with PRNG
auto gen_length = std::bind(rand_length, engine);
auto gen_letter = std::bind(rand_letter, engine);
std::string input;
// input string generator: string of random length, composed of random letters
size_t input_length = gen_length(); // use length generator
for(size_t i=0; i<input_length; ++i) {
// string generation logic...
input.push_back(gen_letter()); // use generator of random letters
}
// ...testing logic
The standard library also contains some functions which can be helpful when generating random inputs:
std::shuffle
- randomly rearranges elements in range using generator.std::sample
- randomly selects elements from the sequence, without replacement.
Note that std::random_shuffle
is now considered obsolete and has been superseded by std::shuffle
.
#
Reference solutionIf the test suite happens to use a reference solution to calculate expected values (which should be avoided when possible), or some kind of reference data like precalculated arrays, etc., it must not be possible for the user to call it, redefine, overwrite, or directly access its contents. To prevent this, reference data can be made a private member of the Describe
structure. A reference solution can be either a private member of Describe
, or a lambda-initialized local variable of an It
block. See [Example test suite](#example-test-suite)
for some examples.
The reference solution or data must not be defined in the Preloaded code.
#
Input mutationGeneral guidelines for submission tests contain a section related to input mutation and how to prevent users from abusing it to work around kata requirements. It's usually not a problem in "real world" C++ programming, but on Codewars, users can take advantage of vulnerable test suites and modify their behavior. Constness of a function argument can be forcefully cast away by a user, or they can change the function signature and remove the const
qualifier from input parameter(s). After calling a user solution, tests should not rely on the state of such values, and they should consider them as potentially modified by the user.
#
Calling assertionsSnowhouse provides a set of useful assertions, but when used incorrectly, they can cause a series of problems:
- Use of an assertion not suitable for the given case may lead to incorrect test results,
- Incorrectly used assertions may produce confusing or unhelpful messages.
To avoid the above problems, calls to assertion functions should respect the following rules:
- Appropriate assertion functions should be used for each given test.
AssertEquals
is not the only option, and Snowhouse provides a selection of constraints and expressions suitable for many scenarios:EqualsWithDelta
for floating point comparisons,EqualsContainer
to compare containers with a predicate, etc. - Overloads of
Assert::That
which acceptmessage_supplier
should be preferred. Assertion message should provide meaningful details on the cause of failure, like test input, etc. Assert::That(bool)
should not be used, because it generates poor feedback on failure. The overloadAssert::That(bool_value, Equals(expected_value), message_supplier)
should be used instead.- Some additional attention should be paid to the order of parameters passed to
Assert::That
. Authors quite often happen to confuseactual
andexpected
in assertion messages. For the C++ testing framework, the order is(actual, SomeConstraint(expected))
. - To avoid unexpected crashes in tests, it's recommended to perform some additional assertions before assuming that the answer returned by the user solution has some particular form or size. For example, if the solution returns a pointer, an explicit assertion should be added to check whether the returned pointer is valid, and not, for example,
nullptr
; the size of the returned container should be verified before accessing an element which could turn out to be located out of its bounds.
#
Additional librariesCurrently, C++ setup provides just a few additional libraries, but they can come helpful for authors to create better kata. For example, {fmt}
library can be used to compose assertion messages and provide better feedback from tests.
#
TestabilityIn C++, not everything can be easily tested. It's not possible to reliably verify the validity of a returned pointer. It's difficult to test for conditions which result in a crash or undefined behavior. Sometimes the only way is to skip some checks or crash the tests.
#
PreloadedC++ sometimes requires some boilerplate code to implement non-trivial tests, checks, and assertions. It can be tempting to insert some code that would be common to sample tests and submission tests in the Preloaded snippet, but this approach sometimes proves to be problematic (see here why), and can cause some headaches for users who are interested in training on the kata locally, or checking how the user solution is called, etc. It's strongly discouraged to use preloaded code to make the code common for test snippets if it would hide some information or implementation details interesting to the user.
Preloaded should not contain #include
directives and using
directives just to makie some names available to other snippets (see using
directives and header files). Preloaded of following form is bad, and should not be used:
// include them here, because all snippets use them
#include <vector>
#include <string>
using namespace std;
#
Example test suiteBelow you can find an example test suite that covers most of the common scenarios mentioned in this article. Note that it does not present all possible techniques, so actual test suites can use a different structure, as long as they keep to established conventions and do not violate authoring guidelines.
//include all required headers
#include <fmt/core.h>
#include <fmt/ranges.h>
#include <vector>
#include <algorithm>
#include <random>
#include <sstream>
#include <string>
Describe(FixedTests) {
// a test case of fixed_tests suite for a primary scenario
It(ExampleArray) {
// using directive limited to non-global scope
using namespace std;
vector<int> items = { 0, 1, 2, 3, 4 };
vector<int> expected = { 0, 1, 4, 9, 16 };
auto actual = square_every_item(items);
// Assertion constraint checking for container equality
Assert::That(actual, EqualsContainer(expected), ExtraMessage("Invalid answer for { 0, 1, 2, 3, 4 }"));
}
// a test case of fixed_tests suite for a potential edge case
It(EmptyArray) {
std::vector<int> empty;
auto actual = square_every_item(empty);
Assert::That(actual, IsEmpty(), ExtraMessage("Input: empty vector"));
}
};
Describe(RandomTests) {
private:
std::mt19937 engine{ std::random_device{}() };
std::function<int ()> gen_number = std::bind(std::uniform_int_distribution<int >{ 1, 100 }, engine);
std::function<size_t()> gen_small_size = std::bind(std::uniform_int_distribution<size_t>{ 2, 10 }, engine);
std::function<size_t()> gen_large_size = std::bind(std::uniform_int_distribution<size_t>{ 80, 100 }, engine);
// random test case generator
std::vector<int> generate_random_input(size_t size) {
std::vector<int> generated;
std::generate_n(std::back_inserter(generated), size, gen_number);
return generated;
}
std::vector<int> square_every_item_ref(const std::vector<int>& numbers) {
std::vector<int> result = numbers;
std::transform(result.begin(), result.end(), result.begin(), [](int a) { return a * a; });
return result;
}
std::string stringify_input(const std::vector<int>& input) {
return fmt::format("{}", fmt::join(input, ","));
}
public:
// a set of small random tests, with verbose debugging messages
It(SmallArrays) {
for(int i=0; i<10; ++i) {
// generate test case
size_t input_size = gen_small_size();
auto input = generate_random_input(input_size);
// tests need to copy the input vector, because
// it is used after calling the user and reference solution
auto original = input;
auto expected = square_every_item_ref(input);
auto actual = square_every_item(input);
// assertion uses a custom message to avoid confusing test output.
// it also uses data from original, non-mutated input array
Assert::That(actual, EqualsContainer(expected), ExtraMessage(fmt::format("Input: {}", stringify_input(original))));
}
}
// a set of large random tests, with less detailed debugging messages
It(LargeArrays) {
for(int i=0; i<10; ++i) {
// generate test cases
size_t input_size = gen_large_size();
auto input = generate_random_input(input_size);
auto expected = square_every_item_ref(input);
auto actual = square_every_item(input);
// assertion uses custom message supplier
Assert::That(actual, EqualsContainer(expected), [&]() {
auto [act, exp] = std::mismatch(actual.cbegin(), actual.cend(), expected.cbegin());
auto idx = act - actual.cbegin();
return fmt::format("Invalid answer for inputs of size {}: vectors mismatch at position {}, expected: {}, actual: {}", input_size, idx, *exp, *act);
});
}
}
};