Text as `char` and C-Compatible Buffers
English

Text as char and C-Compatible Buffers

Section 8.1 established the storage story for fixed-size arrays in general. This section narrows that idea to char: string literals, raw C strings, and writable buffers that keep their bytes inline.

That low-level representation still matters in current scpp. std::string exists and is useful when you want owned growable text, but a lot of APIs – especially extern "C" boundaries – still speak in const char* and char[N].

For each runnable example below, save the file as char-buffers.scpp, then build and run it like this:

scpp char-buffers.scpp -o char-buffers
./char-buffers

For examples that are supposed to be rejected, save the file under the descriptive filename shown in the diagnostic block if you want the compiler output to match byte for byte.

String literals are read-only const char*

A string literal gives you borrowed read-only text.

import std;

int main() {
    const char* greeting = "hello";
    std::println("{} {}", greeting, greeting[1]);
    return 0;
}

Output:

hello e

greeting does not own a buffer that main allocated itself. It is just a pointer to read-only bytes that already exist for the program, and indexing that pointer reads individual characters.

A char[N] array owns a writable text buffer inline

When you need bytes that your own function can edit, a fixed-size char array works like any other array from Section 8.1. The one extra rule is that C-style text must end with \0 so another API knows where the text stops.

extern "C" int puts(const char* s);

int main() {
    char word[6]{};
    word[0] = 's';
    word[1] = 'c';
    word[2] = 'p';
    word[3] = 'p';
    word[4] = '!';
    word[5] = '\0';

    [[scpp::unsafe]] {
        puts(word);
    }
    return 0;
}

Output:

scpp!

word owns six char elements directly. The call to puts is inside [[scpp::unsafe]] for the same reason as every other extern "C" call in this book: the compiler cannot inspect the real implementation on the other side.

The final \0 matters just as much as the visible letters. puts keeps reading until it sees that terminator.

Passing a char[N] buffer gives pointer-style access to the same storage

At a call boundary, an array buffer can be passed to a parameter written with array syntax, and the callee updates the caller’s storage in place.

import std;

void make_excited(char text[6]) {
    text[4] = '!';
    return;
}

int main() {
    char word[6]{};
    word[0] = 's';
    word[1] = 'c';
    word[2] = 'p';
    word[3] = 'p';
    word[4] = '?';
    word[5] = '\0';

    make_excited(word);
    const char* text = word;
    std::println("{}", text);
    return 0;
}

Output:

scpp!

make_excited did not receive a fresh copied array. It wrote through access to the same underlying buffer, so word[4] changed in the caller too.

std::string owns growable text and c_str() bridges to C APIs

A fixed-size char[N] buffer is good when the size is known up front. When you want owned text that can grow, use std::string and borrow a C-compatible view only at the boundary that needs it.

import std;

extern "C" int puts(const char* s);

int main() {
    std::string name{"scpp"};
    name.append(" book");

    [[scpp::unsafe]] {
        puts(name.c_str());
    }

    int letters = static_cast<int>(name.size());
    std::println("{}", letters);
    return 0;
}

Output:

scpp book
9

This keeps ownership with name, not with the C API. c_str() gives the borrowed nul-terminated pointer that puts needs, while std::string stays the better tool for dynamically sized text inside ordinary scpp code.

A string literal cannot initialize a mutable char*

A literal is read-only text, so scpp does not let you treat it as a writable buffer.

int main() {
    char* text = "hi";
    return 0;
}

Compiler output:

string_literal_mutable_pointer_fail.scpp:2:18: error: cannot initialize or assign raw pointer 'text' from an incompatible pointer type without an explicit cast

If code intends to write through the pointer, it needs actual writable storage – for example, a char[N] array it owns itself. If code only needs to read the literal, use const char* instead.

The working rules for text buffers today

The next section returns to borrowed views, using std::span to talk about a contiguous buffer without transferring ownership of it.


← Previous: Fixed-Size Arrays · Table of Contents · Next: Borrowed Views with std::span