Best Practice for Fixed Size String Class
Asked Answered
T

2

12

I would like to have a fixed size string class. Ideally, the interface would match the one of std::string with the one difference that the new class never allocates new memory. It is supposed to be a handy class for application cases where allocating new memory should be avoided. The size can be static (known at compile time).

I think there are two ways. The first would be to implement a class around a char array and then implement more or less all the functions that the std::string has. I also would have to implement some operators to create std::strings with a given fixed size string, etc.

The second method, I'm not even sure is possible, would be to inherit from std::string and override all the functions that may change the size of the string. I looked into the basic_string header in Visual Studio and it doesn't seem to be virtual, so I guess this is not the way to go.

What would you say is the best approach for implementing such class?

Telegraphic answered 26/8, 2016 at 17:36 Comment(15)
Using std::string_view in C++17, you could rig up something very simple combining an array of chars and a string view...Plasmagel
Why not just wrap a std::string and just not allow operations that would let it grow?Uptotheminute
Why should it never allocate memory?Tisiphone
template <int T> class fstring : std::string ... figure out the rest ?Epidote
"Why should it never allocate memory?" Audio Processing Loop. One could just not resize a string but I'm working on a framework.Telegraphic
"template <int T> class fstring : std::string ... figure out the rest" I came this far. So you would go with writing a wrapper, hiding all the functions that would change the size?Telegraphic
@ruhigbrauner and override all the functions that may change the size of the string -- Then what you're left with is simply std::array<char, some_number> with just a couple of functions that could probably be implemented using STL algorithms.Blip
Why not just use char arrays with the assortment of C functions (strcpy, stdlen, etc) that comes with the standard library?Fides
fixed size or fixed capacity?Goolsby
@ RichardHodges fixed capacity, length can be up to N char's. @ MustafaOzturk To harness the full potential of object orientation. :)Telegraphic
How about implementing an allocator and then use std::basic_string<CharT, std::char_traits<CharT>, MyFixedSizeAllocator<CharT>> ?Checkrow
Unfortunately, it seems not work Demo. Small String Optimization disallows too short strings, and std::string seems to not adjust allocation size according to allocator::max_size :-/Checkrow
1. Do you want length-k or max-k strings? 2. Do you want to pass your string to methods that take std::string (or const& of it)? 3. Do you need full defense (e.g. library code) or just a quick check that you don't call anything wrong? 4.Nance
4. Do you use it mostly like a container or mostly like a string? 5. Is there any reason behind this 'false inheritance' than having exactly the same interface as std::string? (If no, you can make a macro that defines a delegator fn using perfect fwding.) 6. Do you want the string on heap/stack?Nance
There's now a Boost implementation of this. See this answer.Torrance
W
17

The first would be to implement a class around a char array and then implement more or less all the functions that the std::string has.

This is definitely the way to go. It's easy to write, easy to use, and difficult to misuse.

template <size_t N>
class fixed_string {
    char array[N+1];
    size_t size;

public:
    fixed_string() : size(0) { array[0] = '\0'; }

    // all the special members can be defaulted
    fixed_string(fixed_string const&) = default;
    fixed_string(fixed_string&&) = default;
    fixed_string& operator=(fixed_string const&) = default;
    fixed_string& operator=(fixed_string&&) = default;
    ~fixed_string() = default;

    // ...
};

All the accessors (data, c_str, begin, end, at, operator[]) are one-liners. All the search algorithms are straightforward.

The only real design question is what do you want the mutations to do on failure. That is:

fixed_string<5> foo("abcde");
foo += 'f'; // assert? throw? range-check internally and ignore?
            // just not even add this and instead write a 
            // try_append() that returns optional<fixed_string&>?

There are advantages and disadvantages to design choice, but regardless of which one you pick, the implementation of each function is also going to be very concise.


The second method, I'm not even sure is possible, would be to inherit from std::string and override all the functions that may change the size of the string. I looked into the basic_string header in Visual Studio and it doesn't seem to be virtual, so I guess this is not the way to go.

Whether or not anything in std::string is virtual is irrelevant to the question of whether or not this is a good idea. You would definitely want to start from:

template <size_t N>
class fixed_string : private std::string { ... }
//                  ^^^^^^^^^

Since your type would definitely not fit the is-a relationship with std::string. It's not a std::string, it'd merely be implemented in terms of it. Private inheritance would make this code ill-formed:

std::string* p = new fixed_string<5>();

so you don't have to worry about lack of virtual.

That said, inheriting from string is going to make for a much more complicated, less efficient implementation than just going the direct route, with way more potential pitfalls. It's probably possible to implement such a thing, but I can't see how it would be a good idea.

Waft answered 11/1, 2017 at 18:13 Comment(0)
A
-1

I went ahead and made a simple class that can be built off of. The structure is as this: the base class is a declaration only interface that will only contain the signatures of the types of constructors you want to have, and a list of all the functions that must be implemented in the inheriting class - classes since they are purely virtual. The derived class is a template class that has the actual implementations. You don't use the class directly since there is a helper function template that takes the type you want to pass for each constructor type that you want to support and it returns that type.

#ifndef FIXED_STRING_H
#define FIXED_STRING_H

#include <string>

// This base class does not contain any member variables 
// and no implementations of any constructor or function
// it serves as a definition to your interface as well as
// defining what methods must be implemented.
class fixed_string_base {
protected:
    // The types of constructors you want to implement
    template<size_t fixed_size>
    explicit fixed_string_base( const char(&words)[fixed_size] ) {};

    // The types of things you want to leave to default
    fixed_string_base() = default;
    fixed_string_base( fixed_string_base const& ) = default;
    fixed_string_base( fixed_string_base&& ) = default;
    fixed_string_base& operator=( fixed_string_base const& ) = default; 
    fixed_string_base& operator=( fixed_string_base&& ) = default;
    virtual ~fixed_string_base() = default;
public:
    // Put all of your pure virtual methods here that fixed_string must implement;
    virtual char* c_str() = 0;
    virtual size_t size() const = 0;
    virtual size_t count() const = 0;
};

// This is the actual class that inherits from its non
// templated declaration interface that has the implementation of the needed constructor(s)
// and functions or methods that were declared purely virtual in the base class
template<size_t fixed_size>
class fixed_string_t  : public fixed_string_base {
private:
    size_t fixed_string_size_t = fixed_size;
    char fixed_string_[fixed_size];

public:
    //template<size_t fixed_size>
    explicit fixed_string_t( const char(&words)[fixed_size] ) {
        strncpy_s( fixed_string_, sizeof(char) * (fixed_size), &words[0], fixed_string_size_t );
        fixed_string_[fixed_size] = '\0';
    }

    // c_str returns the character array.
    virtual char*  c_str() { return fixed_string_; }
    // size gives the total size including the null terminator
    virtual size_t size() const { return fixed_string_size_t; } 
    // count gives the size of the actual string without the null terminator
    virtual size_t count() const { return fixed_string_size_t - 1; }

    // Defaulted Constructors and Operators
    fixed_string_t( fixed_string_t const& ) = default;
    fixed_string_t( fixed_string_t&& ) = default;
    fixed_string_t& operator=( fixed_string_t const& ) = default;
    fixed_string_t& operator=( fixed_string_t&& ) = default;
    virtual ~fixed_string_t() = default;

};

// Helper - Wrapper Function used to create the templated type
template<size_t fixed_size>
fixed_string_t<fixed_size> fixed_string(  const char(&words)[fixed_size] ) {
    return fixed_string_t<fixed_size>( words );
}

#endif // FIXED_STRING_H

To use it would look something like this:

#include <iostream>
#include "FixedString.h"

int main() {
    auto c = fixed_string( "hello" );
    std::cout << c.c_str() << " has a size of " c.size() << " with\n"
              << "a character count of " << c.count() << std::endl;
    return 0;
}

The only thing with this as it currently stands is that this entity is not modifiable. The string itself is fixed. This is just a pattern or a demonstration of what a design pattern of the type that you are looking for might be. You could add to this or expand it, borrow from it or even completely disregard it. The choice is yours.

Almucantar answered 14/1, 2017 at 0:54 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.