10
\$\begingroup\$

Using the standard C library, I want to repeatedly concatenate onto a fixed size string. Despite its promising name, the semantics of strncat do not appear suited to doing this, and the return value provides no new information. strncpy also requires a strlen to update the position. The reason I used snprintf (C99) in the code below is that it returns the number of characters written, which I can subtract from future calls. I've written a helper struct:

#include <stdlib.h> /* EXIT_SUCCESS */
#include <stdio.h>  /* *printf */
#include <string.h> /* strlen */

struct Supercat {
    char *print, *cursor;
    size_t left;
    int is_truncated;
};

/** Initialises {cat} to hold the string {print}, size {print_size}. It stores
 the empty string in print. */
static void supercat_init(struct Supercat *const cat, char *const print,
    const size_t print_size) {
    cat->print        = cat->cursor = print;
    cat->left         = print_size;
    cat->is_truncated = 0;
    print[0] = '\0';
}

/** Adds {append} to the string specified when \see{supercat_init} was
 called. If {append} is too big for the size, it truncates the string and sets
 {cat.is_truncated}. */
static void supercat(struct Supercat *const cat, const char *const append) {
    size_t size_took;
    int took;

    if(cat->is_truncated) return;
    took = snprintf(cat->cursor, cat->left, "%s", append);
    if(took < 0)  { cat->is_truncated = -1; return; }
    if(took == 0) { return; }
    if((size_took = took) >= cat->left) {
        cat->is_truncated = -1, size_took = cat->left - 1;
    }
    cat->cursor += size_took, cat->left -= size_took;
}

static const char *const start_str     = "[ ";
static const char *const end_str       = " ]";
static const char *const alter_end_str = "...]";
static const char *const sep_str       = ", ";
static const char *const null_str      = "Null";

int main(int argc, char **argv) {
    static char buffer[80];
    struct Supercat cat;
    int i;

    /* want to terminate before the end; always have space for alter_end_str */
    supercat_init(&cat, buffer, sizeof buffer - strlen(alter_end_str));

    if(!argc) {
        supercat(&cat, null_str);
    } else {
        supercat(&cat, start_str);
        for(i = 0; i < argc; i++) {
            if(i) supercat(&cat, sep_str);
            supercat(&cat, argv[i]);
            if(cat.is_truncated) break;
        }
        /* we are guaranteed to have enough room */
        sprintf(cat.cursor, "%s", cat.is_truncated ? alter_end_str : end_str);
    }

    printf("Arguments: %s.\n", cat.print);

    return EXIT_SUCCESS;
}

The string now will go up to 79 characters and refuse to write any more:

Thor:Supercat neil$ bin/Supercat 
Arguments: [ bin/Supercat ].
Thor:Supercat neil$ bin/Supercat saves the day!
Arguments: [ bin/Supercat, saves, the, day! ].
Thor:Supercat neil$ bin/Supercat where are you? we need you to rescue us from the burning building.
Arguments: [ bin/Supercat, where, are, you?, we, need, you, to, rescue, us, from, the,...].
Thor:Supercat neil$ bin/Supercat where are you? we need you to rescue us from this burning building.
Arguments: [ bin/Supercat, where, are, you?, we, need, you, to, rescue, us, from, this...].
Thor:Supercat neil$ bin/Supercat where are you? we need you to rescue us from danger.
Arguments: [ bin/Supercat, where, are, you?, we, need, you, to, rescue, us, from, dang...].

Using a temporary structure is a pain, but it seems to me like writing something like if(strlen(strncat(cat, argv[i], sizeof cat - strlen(cat))) >= sizeof buffer) break; would be inefficient on large strings because it traverses the string 3 times (I count) to write one value. On the other hand, snprintf is overkill with a constant format string.

\$\endgroup\$
3
+50
\$\begingroup\$

Back in the day, even before the official C89 standard was released, people were asking "why the hell does strcat not return the end pointer?"

I believe that the C standards committee, in a fit of pique, "doubled down" on their mistake and made a willful effort not to remedy it, simply because it would be admitting that they made a boneheaded error in the first place. It's been a sore spot ever since, and now they've switched to strcat_s which doesn't return a pointer at all, which leaves the problem unresolved, and leaves strcat as an unloved and underused function.

Also pretty much from the get-go, there were libraries of string functions that provided an alternative version of strcat that did the right thing and returned a pointer to the end of the string. Often, those functions were named strecat or strcate, with the e meaning "return a pointer to the end." There were also strecpy and various strne___ functions. (IIRC, Borland provided them with their Turbo C++ compiler.)

Considering that you know the size of the buffer, I'd suggest you simply write (or search for) a version of strnecpy and strnecat and maintain your own count of characters remaining.

I suppose you could maintain both pointer and count in static variables, and accept a NULL destination to mean "use the previous values". You should read up on strtok first, though, to understand the implications - in general that sort of code isn't thread-safe, interrupt-safe, etc. (strtok has nothing to do with strcat but it's defined to have similar "use the last string" behavior, and usually comes with a bunch of caveats in the documentation.)

register char *p = buffer;
register char *pe = buffer + sizeof(buffer);

p = strnecpy(p, start_str, pe - p);

for (i = 0; i < argc; ++i) {
    if (i) p = strnecat(p, sep_str, pe - p);
    p = strnecat(p, argv[i], pe - p);

    if (p >= pe - 1) break;
}

Or, if a NULL destination means "re-use the last pointer and size info" you might have:

strnecpy(buffer, start_str, sizeof(buffer));

for (i = 0; i < argc; ++i) {
    if (i) strnecat(NULL, sep_str);
    if (strnecat(NULL, argv[i]) >= buffer + sizeof(buffer) - 1) break;
}

FWIW, I actually think this last bit would be a bad idea, simply because it relies so very much on hidden behavior. It's not at all clear, just reading the code, that there's a bunch of secret-squirrel things happening in the background. With a C++ string class, that's okay - with C strings, secret accounting is surprising.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.