Boost logo

Boost :

From: Peter Dimov (pdimov_at_[hidden])
Date: 2019-12-04 15:59:51


This is my review of FixedString.

* Summary

The library should be ACCEPTED. It's useful, for more than one domain, and
specifically where compile-time string manipulation is concerned, everyone
needs to reinvent it.

The name fixed_string is fine as is. Alternatives exist, but I don't see any
of them as unquestionably better, and one becomes accustomed to
"fixed_string" after a very brief period.

Throwing on exceeding capacity is fine as is.

A function should be provided to check the remaining capacity, because
writing this check by hand is very easy to get wrong, and in fact the
library throughout its implementation consistently gets it wrong.

* Documentation

The documentation is adequate, but I don't find the reference convenient. I
would prefer a synopsis that contains the entire class definition of
`fixed_string`, so that I can see the various overloads at a glance.
Instead, the synopsis only gives me the declaration of `fixed_string`,
without even telling me what namespace it's in. (It also gives me
`basic_string_view`, also without mentioning the namespace.)

The current layout is fine if I want to look up a specific function by name.

The format should ideally follow the standard library, with Throws and
Expects clauses (the latter instead of "undefined behavior unless".)

It's not documented when constructors and assignment operators throw.

* Design

- static_capacity should be removed; capacity() and max_size() should be
static constexpr.

- all nonthrowing functions should be marked `noexcept`.

- initializer_list<CharT> overloads are of dubious utility. I know that the
design copies std::string here, but I think that we should deviate and not
provide them.

- substr() should return a fixed_string, subview() a string_view (as already
implemented.)

- operator+ should be provided, only for arguments with capacity known at
compile time (fixed_string<N>, CharT and CharT const (&)[ N ]), with the
capacity of the result being the sum of the capacities of the arguments.
This is not very useful for long strings, but it's fine for short strings,
useful for compile-time manipulation, and useful in simple cases such as
to_fixed_string( n ) + " bytes written".

- a function that checks whether the remaining capacity is at least n should
be added. I suggest the name

    bool can_fit( std::size_t n ) const noexcept;

although my previous choice of "has_capacity_for" has merit too.

This is, in my opinion, an absolutely necessary addition, because of my
already-stated observation that the check is almost always gotten wrong.
Here for instance is an example:

    if(size() + count > max_size())
        BOOST_FIXED_STRING_THROW(std::length_error{
            "size() + count > max_size()"});

The correct way to write it is max_size() - size() > count, which avoids the
integer overflow in size() + count. In this case, passing count = (size_t)-1
will pass the check (when size() > 0) and then lead to undefined behavior.

This makes the utility of reserve() dubious, because it basically invites
one to get it wrong, but I think we should retain it for consistency.

- everything should ideally be constexpr. The problem here is that constexpr
requires initializing all elements, and this will heavily penalize runtime
uses of f.ex. fixed_string<512>. On compilers with
__builtin_is_constant_evaluated (gcc 9, clang 9) we should use that;
otherwise, it might be worth it to create a specialization for N < some
suitable upper limit.

Again, any nontrivial string manipulation at compile time basically requires
a fixed_string.

* Implementation

The physical separation of the library into three headers is inconvenient. A
single header would make it possible to include the library directly on
Godbolt via https, which is useful for both demonstrations and analysis of
the generated code. In addition, hunting down the definitions is irritating
for people reading the code.

The reverse is true for the tests; they should be split into files each
testing a logical group, such as constructors, assignment, append, and so
on.

The helper functions "testAS", "testI" and so on make reading the tests
unnecessarily cryptic (and is quite annoying for reviewers.) They should
have been named "test_assign", "test_insert".

It's generally better to use `BOOST_TEST_EQ(s.size(), 0);` instead of
`BOOST_TEST(s.size() == 0);` because the former prints the values on
failure, which often gives clues as to where the error is.

The test functions shouldn't be in the boost::fixed_string namespace. Users
don't write their code in this namespace, so the tests don't test what users
use.

All capacity checks should be either audited for correctness and fixed, or
use the proposed can_fit addition.

Instead of repeating

        BOOST_FIXED_STRING_THROW(std::length_error{
            "n > max_size()"});

in every function, which leads to it being inlined there repeatedly, this
could use a helper function

    void throw_length_error( char const * msg, boost::source_location const&
loc )
    {
        boost::throw_exception( std::length_error( msg ), loc );
    }

taking advantage of the new boost::throw_exception overload that takes a
source location as documented in
https://www.boost.org/doc/libs/develop/libs/throw_exception/doc/html/throw_exception.html.

A variation could take `n` as an argument and compose a message containing
it, f.ex.

    void throw_length_error_fmt( boost::source_location const& loc, char
const * fmt, ... );

#define BOOST_FIXED_STRING_USE_BOOST should go, as already mentioned.

Since the library requires C++11, there's no need to use
BOOST_STATIC_ASSERT. static_assert is fine. Unless of course we backport it
to C++03, which might be doable, although of questionable utility.

Checking for overlap and using Traits::copy instead of Traits::move isn't
needed. Traits::move does the same check anyway, so there's no gain, just an
opportunity to get the check wrong and introduce undefined behavior.

"The behavior is undefined if `count >= npos`" doesn't seem necessary. In
this case, count will be bigger than max_size() and the constructor will
throw std::legth_error. There's no reason to gratuitously introduce
undefined behavior.

All "undefined behavior" cases should consistently BOOST_ASSERT.


Boost list run by bdawes at acm.org, gregod at cs.rpi.edu, cpdaniel at pacbell.net, john at johnmaddock.co.uk