7
\$\begingroup\$

I am currently developing a terminal-based listview control, that uses ncurses for rendering. The main goal for the listview was to be intuitive, performant, and reusable for other projects.

An example, is shown below:

listview.c

#include "listview.h"
#include <ncurses.h>
#include <string.h>
#include <stdint.h>
#include <stdlib.h>
#include <stdarg.h>

listview *listview_new() {
    listview *lv = malloc(sizeof(listview));
    if (!lv) { return NULL; }

    lv->rows = NULL;
    lv->rows_count = 0;
    lv->columns = NULL;
    lv->column_count = 0;
    lv->listview_max_width = 0;
    lv->listview_max_height = 0;
    lv->cell_padding = 2;

    return lv;
}

void listview_add_column(listview *lv, char *text) {
    lv->columns = realloc(lv->columns, (lv->column_count + 1) * sizeof(lv_column));
    if (!lv->columns) { return; }

    lv->columns[lv->column_count].text = text;
    lv->columns[lv->column_count].alignment = left;
    lv->columns[lv->column_count].width = (2 * lv->cell_padding) + strlen(text);

    lv->column_count++;
}

int listview_set_column_alignment(listview *lv, int column_index, column_alignment alignment) {
    if (column_index >= lv->column_count)
        return -1;

    lv->columns[column_index].alignment = alignment;
    return 0;
}

void listview_render(WINDOW *win, listview *lv) {
    // Define colors
    wclear(win);
    init_pair(10, COLOR_BLACK, COLOR_WHITE);
    init_pair(20, COLOR_WHITE, COLOR_BLACK);
    getmaxyx(win, lv->listview_max_height, lv->listview_max_width);
    wattron(win, COLOR_PAIR(10));

    // Render columns first
    int cursor_pos = 0;
    for (int i = 0; i < lv->column_count; i++) {
        char *col_text = lv->columns[i].text;
        int col_width = lv->columns[i].width;
        int col_fill = col_width - (2 * lv->cell_padding) - strlen(col_text);
        int extra_pad = lv->listview_max_width - cursor_pos - (lv->cell_padding + strlen(col_text) + lv->cell_padding);

        if (i == lv->column_count - 1) {
            mvwprintw(win, 0, cursor_pos, "%*s%s%*s%*s", lv->cell_padding, "", col_text, lv->cell_padding, "",
                      extra_pad, "");
            cursor_pos = 0;
            break;
        } else {
            if (lv->columns[i].alignment == left) {
                mvwprintw(win, 0, cursor_pos, "%*s%s%*s%*s", lv->cell_padding, "", col_text, lv->cell_padding, "",
                          col_fill,
                          "");
            } else if (lv->columns[i].alignment == right) {
                mvwprintw(win, 0, cursor_pos, "%*s%*s%s%*s", lv->cell_padding, "", col_fill, "", col_text,
                          lv->cell_padding, "");
            } else if (lv->columns[i].alignment == center) {
                int col_fill_left = col_fill / 2;
                int coll_fill_right = col_fill - col_fill_left;
                mvwprintw(win, 0, cursor_pos, "%*s%*s%s%*s%*s", lv->cell_padding, "", col_fill_left, "", col_text,
                          coll_fill_right, "", lv->cell_padding, "");
            }
        }
        cursor_pos += lv->cell_padding + strlen(col_text) + lv->cell_padding + col_fill;
    }
    wattron(win, COLOR_PAIR(20));

    // Render rows
    for (int i = 0; i < lv->rows_count; i++) {
        for (int j = 0; j < lv->rows[i].cell_count; j++) {
            char *cell_text = lv->rows[i].cells[j].text;
            int col_width = lv->columns[j].width;
            int col_fill = col_width - (2 * lv->cell_padding) - strlen(cell_text);
            int extra_pad =
                    lv->listview_max_width - cursor_pos - (lv->cell_padding + strlen(cell_text) + lv->cell_padding);

            int color_id = alloc_pair(lv->rows[i].cells[j].fg_color, lv->rows[i].cells[j].bg_color);
            wattron(win, COLOR_PAIR(color_id));
            if (j == lv->rows[i].cell_count - 1) {
                mvwprintw(win, i + 1, cursor_pos, "%*s%s%*s%*s", lv->cell_padding, "", cell_text, lv->cell_padding,
                          "", extra_pad, "");
                cursor_pos = 0;
                break;
            } else {
                if (lv->columns[j].alignment == left) {
                    mvwprintw(win, i + 1, cursor_pos, "%*s%s%*s%*s", lv->cell_padding, "", cell_text, lv->cell_padding,
                              "", col_fill, "");
                } else if (lv->columns[j].alignment == right) {
                    mvwprintw(win, i + 1, cursor_pos, "%*s%*s%s%*s", lv->cell_padding, "", col_fill, "", cell_text,
                              lv->cell_padding, "");
                } else if (lv->columns[j].alignment == center) {
                    int col_fill_left = col_fill / 2;
                    int coll_fill_right = col_fill - col_fill_left;
                    mvwprintw(win, i + 1, cursor_pos, "%*s%*s%s%*s%*s", lv->cell_padding, "", col_fill_left, "",
                              cell_text, coll_fill_right, "", lv->cell_padding, "");
                }
            }
            free_pair(color_id);
            wattroff(win, COLOR_PAIR(color_id));
            cursor_pos += lv->cell_padding + strlen(cell_text) + lv->cell_padding + col_fill;
        }
    }
}

int listview_delete_column(listview *lv, int column_index)
{
    if(column_index >= lv->column_count)
        return -1;

    // Remove column
    lv_column *new_cols = malloc(sizeof(lv_column) * (lv->column_count - 1));
    memmove(new_cols, lv->columns, column_index * sizeof(lv_column));
    memmove(&new_cols[column_index], &lv->columns[column_index + 1], (lv->column_count - column_index - 1) * sizeof(lv_column));
    free(lv->columns);
    lv->columns = new_cols;
    
    // Remove effected cells in every row
    for (int i = 0; i < lv->rows_count; i++)
    {
        lv_cell *new_cells = malloc(sizeof(lv_cell) * (lv->column_count - 1));
        memmove(new_cells, lv->rows[i].cells, column_index * sizeof(lv_cell));
        memmove(&new_cells[column_index], &lv->rows[i].cells[column_index + 1], (lv->column_count - column_index - 1) * sizeof(lv_cell));
        
        free(lv->rows[i].cells);
        lv->rows[i].cells = new_cells;
        lv->rows[i].cell_count--;
    }
    
    lv->column_count--;

    return 0;
}

int listview_update_cell_background_color(listview *lv, int row_index, int cell_index, lv_color bg_color) 
{
    if (bg_color < 0 || bg_color > 7)
        return -1;
    lv->rows[row_index].cells[cell_index].bg_color = bg_color;
    return 0;
}

int listview_update_cell_foreground_color(listview *lv, int row_index, int cell_index, lv_color fg_color) 
{
    if (fg_color < 0 || fg_color > 7)
        return -1;
    lv->rows[row_index].cells[cell_index].fg_color = fg_color;
    return 0;
}

void listview_add_row(listview *lv, ...) {
    va_list argp;
    va_start(argp, lv);

    lv->rows = realloc(lv->rows, sizeof(lv_row) * (lv->rows_count + 1));
    lv->rows[lv->rows_count].cells = calloc(lv->column_count, sizeof(lv_cell));
    lv->rows[lv->rows_count].cell_count = lv->column_count;

    for (int i = 0; i < lv->column_count; i++) {
        char *text = va_arg(argp,
        char*);
        lv->rows[lv->rows_count].cells[i].text = text;
        lv->rows[lv->rows_count].cells[i].bg_color = black;
        lv->rows[lv->rows_count].cells[i].fg_color = white;
    }

    lv->rows_count++;
    va_end(argp);

    update_max_col_width(lv, lv->rows_count);
}

int listview_delete_row(listview *lv, int row_index) {
    if (row_index > lv->rows_count)
        return - 1;

    lv_row *new_rows = malloc(sizeof(lv_row) * (lv->rows_count - 1));
    if (new_rows == NULL)
        return -2;

    memmove(new_rows, lv->rows, row_index * sizeof(lv_row));
    memmove(&new_rows[row_index], &lv->rows[row_index + 1], (lv->rows_count - row_index - 1) * sizeof(lv_row));

    free(lv->rows);
    lv->rows = new_rows;
    lv->rows_count--;

    update_max_col_width(lv, lv->rows_count);
    return 0;
}

int listview_insert_row(listview *lv, int row_index, ...) {
    if (row_index > lv->rows_count)
        return -1;

    va_list argp;
    va_start(argp, row_index);

    lv->rows = realloc(lv->rows, sizeof(lv_row) * (lv->rows_count + 1));
    if(!lv->rows)
        return -2;

    if (row_index < lv->rows_count) {
        int entries_to_move = (lv->rows_count - row_index);
        memmove(&lv->rows[row_index + 1], &lv->rows[row_index], entries_to_move * sizeof(lv_row));
        lv->rows[row_index].cells = (lv_cell *) malloc(sizeof(lv_cell) * lv->column_count);
        for (int i = 0; i < lv->column_count; i++) {
            lv->rows[row_index].cell_count = lv->column_count;
            lv->rows[row_index].cells[i].text = va_arg(argp, char*);
            lv->rows[row_index].cells[i].fg_color = white;
            lv->rows[row_index].cells[i].bg_color = black;
        }
    } else if (row_index == lv->rows_count) {
        lv->rows[row_index].cells = (lv_cell *) malloc(sizeof(lv_cell) * lv->column_count);
        for (int i = 0; i < lv->column_count; i++) {
            lv->rows[row_index].cell_count = lv->column_count;
            lv->rows[row_index].cells[i].text = va_arg(argp, char*);
            lv->rows[row_index].cells[i].fg_color = white;
            lv->rows[row_index].cells[i].bg_color = black;
        }
    }

    lv->rows_count++;
    update_max_col_width(lv, lv->rows_count);

    return 0;
}

int listview_update_cell_text(listview *lv, int row_index, int cell_index, char *text) {
    if (row_index >= lv->rows_count || cell_index >= lv->column_count)
        return -1;

    if(text == NULL)
        return -2;

    lv->rows[row_index].cells[cell_index].text = text;
    update_max_col_width(lv, lv->rows_count);
    return 0;
}

void update_max_col_width(listview *lv, int row_index) {

    // Make sure index is valid
    if (row_index > lv->rows_count || row_index < 0)
        return;

    // Reset all max col width values
    for (int i = 0; i < lv->column_count; i++) {
        lv->columns[i].width = strlen(lv->columns[i].text) + (2 * lv->cell_padding);
    }
    // Recompute the max col width values for rows
    for (int i = 0; i < lv->rows_count; i++) {
        for (int j = 0; j < lv->rows[i].cell_count; j++) {
            if (strlen(lv->rows[i].cells[j].text) + (2 * lv->cell_padding) > lv->columns[j].width)
                lv->columns[j].width = strlen(lv->rows[i].cells[j].text) + (2 * lv->cell_padding);
        }
    }
}

void listview_free(listview *lv) {

    // Free rows
    free(lv->rows);

    // Free headers
    free(lv->columns);

    // Free lv itself
    free(lv);
}

listview.h

#ifndef LISTVIEW_H
#define LISTVIEW_H

#include <stdint.h>
#include <stdlib.h>
#include <ncurses.h>

typedef enum lv_color {
    black = COLOR_BLACK,
    blue = COLOR_BLUE,
    cyan = COLOR_CYAN,
    green = COLOR_GREEN,
    magenta = COLOR_MAGENTA,
    red = COLOR_RED,
    white = COLOR_WHITE,
    yellow = COLOR_YELLOW,
} lv_color;

typedef enum column_alignment {

    // Aligns the column text and the cells in that column to the left
    left,

    // Aligns the column text and the cells in that column centered
    center,

    // Aligns the column text and the cells in that column to the right
    right

} column_alignment;

typedef struct lv_cell {

    // The text in the cell
    char *text;

    short bg_color;

    short fg_color;

} lv_cell;

typedef struct lv_column {

    // The column text
    char *text;

    // The width of the column (depends on width of the row cells)
    uint16_t width;

    // Specifies the alignment of the text in entire column
    column_alignment alignment;

} lv_column;

typedef struct lv_row {

    // Holds all cells in the row
    lv_cell *cells;

    // Total amount of cells in the row
    uint8_t cell_count;

} lv_row;

typedef struct listview {

    // Holds all columns
    lv_column *columns;

    // Holds amount of columns
    uint16_t column_count;

    // Holds all rows
    lv_row *rows;

    // Holds amount of rows
    uint16_t rows_count;

    // Maximum window width the listview can occupy
    uint16_t listview_max_width;

    // Maximum window height the listview can occupy
    uint16_t listview_max_height;

    // Amount of spaces padded to the left and right of each cell
    uint8_t cell_padding;

} listview;


listview *listview_new();

void listview_free(listview *lv);

void listview_add_column(listview *lv, char* text);

int listview_set_column_alignment(listview *lv, int column_index, column_alignment alignment);

int listview_insert_row(listview* lv, int row_index, ...);

int listview_delete_column(listview *lv, int column_index);

void listview_add_row(listview *lv, ...);

int listview_delete_row(listview *lv, int row_index);

int listview_update_cell_text(listview *lv, int row_index, int cell_index, char* text);

int listview_update_cell_background_color(listview* lv, int row_index, int cell_index, lv_color bg_color);

int listview_update_cell_foreground_color(listview* lv, int row_index, int cell_index, lv_color fg_color);

void listview_render(WINDOW *win, listview *lv);

void update_max_col_width(listview *lv, int row_index);

#endif

main.c

#include "ui.h"
#include <stdio.h>
#include <stdlib.h>
#include <ncurses.h>
#include "listview.h"

int main()
{
    // Setup stuff
    initscr();
    start_color();
    clear();
    noecho();
    curs_set(0);
    cbreak();
    WINDOW* win = newwin(0, 0, 0, 0);
    refresh();

    // Create listview
    listview* lv = listview_new();
    listview_add_column(lv, "Country");
    listview_add_column(lv, "Capital");
    listview_add_column(lv, "Area");
    listview_add_column(lv, "Population");
    listview_add_column(lv, "Currency");

    // Add some rows of data
    listview_add_row(lv, "Germany", "Berlin", "357,022 km2", "83,190,556", "Euro");
    listview_add_row(lv, "Japan", "Tokyo", "377,975 km2", "125,470,000", "Yen");
    listview_add_row(lv, "Netherlands", "Amsterdam", "41,865 km2", "83,190,556", "Euro");
    listview_add_row(lv, "China", "Beijing", "9,596,961 km2", "1,444,390,177", "Renminbi");
    listview_add_row(lv, "France", "Paris", "640,679 km2", "67,413,000", "Euro");
    listview_add_row(lv, "New Zealand", "Wellington", "268,021 km2", "5,123,760", "New Zealand Dollar");
    listview_render(win, lv);
    wgetch(win);

    listview_update_cell_foreground_color(lv, 0, 0, red);
    listview_update_cell_foreground_color(lv, 0, 2, green);
    listview_update_cell_foreground_color(lv, 0, 4, blue);
    listview_update_cell_background_color(lv, 1, 1, yellow);
    listview_update_cell_background_color(lv, 1, 3, magenta);
    listview_render(win, lv);
    wgetch(win);

    listview_insert_row(lv, 1, "United States of America", "Washington, D.C.", "9,525,067 km2", "331,449,281", "US Dollar");
    listview_render(win, lv);
    wgetch(win);

    listview_delete_column(lv, 2);
    listview_render(win, lv);
    wgetch(win);

    listview_free(lv);
    delwin(win);

    return 0;
}

Make sure to compile the example with -lncurses, to try it out.

While the listview works just fine it its current state, I am sure, there is lots of room for improvement. To me it seems that especially the render function can vastly be improved. However, some feedback regarding my api-design and performance are also welcome :)

\$\endgroup\$

2 Answers 2

7
\$\begingroup\$

General Observations

The code is solid, basically well written, fairly easy to maintain and the code is reusable. There are a few places where some software engineering principles could help, and while this is C rather than an object oriented language there could be a few places where object oriented concepts might decrease some maintenance efforts.

Self Documenting Code

Some of the comments, at least in listview.h are unnecessary. The code should be self documenting as much as possible. Comments such as // Holds all columns and // Holds all rows really aren't necessary. Where you might need comments is in listview.c at the beginning of a function to explain any design decsions that might not be clear just from the code. In listview.h you might want to explain why you are using uint16_t rather than size_t as the size of the columns array and the size of the rows array.

Possible Objects

The listview struct contains lists of lv_row and lv_column. Each of these lists are implemented as a pointer and a count, instead there could be container type of lv_rows and a container type for lv_columns, the container would have the pointer to the data and the count rather than the listview struct having the data. Each of these containers could have methods/functions to implement add, delete and insert. These methods could be added to the container structs using pointers to functions, they don't need to be. Depending on how important performance of adding, deleting and inserting rows and cells are, it might be better to use linked lists rather than arrays. I have not included pointers to functions in the following declarations.

typedef struct lv_row_container
{
    lv_row* rows;
    uint16_t count;
} lv_row_container;

typedef struct lv_column_container
{
    lv_row* columns;
    uint16_t count;

} lv_column_container;

typedef struct listview {
    lv_column_container columns;
    lv_row_container rows;
    uint16_t listview_max_width;
    uint16_t listview_max_height;
    uint8_t cell_padding;
} listview;

Function Naming and Grouping

Currently the code contains the following functions:

void listview_add_column(listview* lv, char* text);
int listview_set_column_alignment(listview* lv, int column_index, column_alignment alignment);
int listview_insert_row(listview* lv, int row_index, ...);
int listview_delete_column(listview* lv, int column_index);
void listview_add_row(listview* lv, ...);
int listview_delete_row(listview* lv, int row_index);

I would suggest the following instead:

void listview_column_add(listview* lv, char* text);
int listview_column_set_alignment(listview* lv, int column_index, column_alignment alignment);
int listview_column_delete(listview* lv, int column_index);
int listview_row_insert(listview* lv, int row_index, ...);
void listview_row_add(listview* lv, ...);
int listview_row_delete(listview* lv, int row_index);

The code for the rows and columns can then be broken up into their own headers and source files at some point in the future. Abstracting the row and column functions could make the listview code easier to maintain.

Nit-picks

Inconsistent Use of lv_color Colors

The header file listview.h contains the definition of the enum lv_color. This is used inconsistently in listview.c. In the function listview_insert_row() the lv_color values are used, however, in the function listview_render() COLOR_BLACK and COLOR_WHITE are used. It would be best to stick with one definition throughout the file.

DRY Code

There is a programming principle called the Don't Repeat Yourself Principle sometimes referred to as DRY code. If you find yourself repeating the same code multiple times it is better to encapsulate it in a function. If it is possible to loop through the code that can reduce repetition as well. In the function listview_render() this code repeats itself in 2 different loops:

        if (i == lv->column_count - 1) {
            mvwprintw(win, 0, cursor_pos, "%*s%s%*s%*s", lv->cell_padding, "", col_text, lv->cell_padding, "",
                extra_pad, "");
            cursor_pos = 0;
            break;
        }
        else {
            if (lv->columns[i].alignment == left) {
                mvwprintw(win, 0, cursor_pos, "%*s%s%*s%*s", lv->cell_padding, "", col_text, lv->cell_padding, "",
                    col_fill,
                    "");
            }
            else if (lv->columns[i].alignment == right) {
                mvwprintw(win, 0, cursor_pos, "%*s%*s%s%*s", lv->cell_padding, "", col_fill, "", col_text,
                    lv->cell_padding, "");
            }
            else if (lv->columns[i].alignment == center) {
                int col_fill_left = col_fill / 2;
                int coll_fill_right = col_fill - col_fill_left;
                mvwprintw(win, 0, cursor_pos, "%*s%*s%s%*s%*s", lv->cell_padding, "", col_fill_left, "", col_text,
                    coll_fill_right, "", lv->cell_padding, "");
            }
        }

It might be better to put that code into a function that needs to be written and debugged only once. This makes the code easier to maintain and more reusable as well.

Complexity

There are two functions that are too complex (does too much), listview_render() is one and main() is the other. As programs grow in size the use of main() should be limited to calling functions that parse the command line, calling functions that set up for processing, calling functions that execute the desired function of the program, and calling functions to clean up after the main portion of the program.

There is also a programming principle called the Single Responsibility Principle that applies here. The Single Responsibility Principle states:

that every module, class, or function should have responsibility over a single part of the functionality provided by the software, and that responsibility should be entirely encapsulated by that module, class or function.

It is possible that the suggestion about DRY Code above will correct the issue in listview_render(), however the function could also be implemented by calling 2 additional functions, render_columns() and render_rows().

\$\endgroup\$
3
  • \$\begingroup\$ Thanks for the feedback, definitely some useful suggestions in there. However, I would love to get some more indepth-feedback on how I can improve specifically the rendering method. Outlining rendering of rows and columns in separate methods, wont really do that much. I feel like there is much more, that can be done. Especially, since mvwprintw gets called multiple times in there, but with different formatting. \$\endgroup\$
    – 766F6964
    Jun 30, 2021 at 11:50
  • 1
    \$\begingroup\$ @766F6964 It may not look like it does much, but it cleans up your code enough that it both shows the remaining problems and how to clean up those problems. Cleaning up those mwprintw starts with applying more of the same principles outlined in this answer. \$\endgroup\$
    – Mast
    Jul 1, 2021 at 6:44
  • \$\begingroup\$ @Mast I have now extracted the rendering in two methods, one for the columns and one for the rows, however that still leaves me with 4 long mvwprintw calls. While I could outline the mvwprintw to a separate function too, that does just that, it wouldn't help much, because I would have to pass all the arguments through that I currently pass to the mvwprintw function, because they are all different. That makes it a bit tricky to refactor that. \$\endgroup\$
    – 766F6964
    Jul 2, 2021 at 21:29
4
+25
\$\begingroup\$

You have a thorough review already; I'll just add some minor points.


We have a pointer to char in the cell structure:

typedef struct lv_cell {
    // The text in the cell
    char *text;
    …
} lv_cell;

However, we don't need to modify the contents of the string, and making it const char* (and updating function signatures to match) would allow us to pass string literals without the compiler warnings we currently get. (If you're not seeing warnings, then this should be a prompt to compile with maximal warnings enabled!)


listview *listview_new();

We should declare what arguments are accepted - in this case, don't allow any to be passed:

listview *listview_new(void);

    listview *lv = malloc(sizeof(listview));

A good practice, that reduces the likelihood of bugs when the code is edited, is to use an expression, rather than a type, as argument to sizeof:

    listview *lv = malloc(sizeof *lv);
    if (!lv) {
        /* caller must deal with failed allocation */
        return lv;
    }

We leak memory when realloc() returns a null pointer:

lv->columns = realloc(lv->columns, (lv->column_count + 1) * sizeof(lv_column));
if (!lv->columns) { return; }

When the allocation fails, the memory is still allocated, but we've overwritten our pointer with null, so we are no longer able to free() it. We need to use a two-step reallocation:

void *new = realloc(lv->columns, (sizeof *lv->columns) * (lv->column_count + 1));
if (!new) { return; }
lv->columns = new;

We ought to return a value from this function, to tell the caller whether we were successful or not.

There are other malloc() and realloc() calls with poor handling of failure; I won't list them all.


There's needless allocation here:

lv_column *new_cols = malloc(sizeof(lv_column) * (lv->column_count - 1));
memmove(new_cols, lv->columns, column_index * sizeof(lv_column));
memmove(&new_cols[column_index], &lv->columns[column_index + 1], (lv->column_count - column_index - 1) * sizeof(lv_column));
free(lv->columns);
lv->columns = new_cols;

We only need to shrink the existing allocation:

--lv->column_count;
memmove(lv->columns + column_index, lv->columns + column_index + 1,
        (sizeof *lv->columns) * (lv->column_count - column_index));
lv->columns = realloc(lv->columns, (sizeof *lv->columns) * lv->column_count);
assert(lv->columns); /* shrinking always succeeds */

There are some other instances of this pattern, which should also be adjusted.

As this seems not to be clear, here's a full-program demonstration (using a local array in lieu of allocated memory):

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

int main(int argc, char **argv)
{
    const char *a[] = {
        "zero", "one", "two", "three", "four",
        "five", "six", "seven", "eight", "nine",
    };
    size_t a_len = sizeof a / sizeof *a;
    for (char **s = argv + 1;  a_len && *s;  ++s) {
        char *e;
        long n = strtol(*s, &e, 0);
        if (e == *s) { n = 5; }
        if (n < 0) { n = 0; }
        if (n >= a_len) { n = a_len - 1; }

        printf("Removing element %li: ", n);
        {
            /* actually do it */
            --a_len;
            memmove(a+n, a+n+1, (sizeof *a) * (a_len - n));
        }

        for (size_t i = 0;  i < a_len;  ++i) {
            printf("%s ", a[i]);
        }
        puts("");
    }
}

Example output:

./263470 2 3 0 10
Removing element 2: zero one three four five six seven eight nine 
Removing element 3: zero one three five six seven eight nine 
Removing element 0: one three five six seven eight nine 
Removing element 6: one three five six seven eight 

The replacement of listview_delete_column() is then:

int listview_delete_column(listview *lv, int column_index)
{
    if (column_index >= lv->column_count && column_index < 0) {
        return -1;
    }

    lv->column_count--;
    // Remove column
    memmove(lv->columns + column_index, lv->columns + column_index + 1,
            (sizeof *lv->columns) * (lv->column_count - column_index));
    lv->columns = realloc(lv->columns, (sizeof *lv->columns) * lv->column_count);

    // Remove affected cell from each row
    for (int i = 0;  i < lv->rows_count;  ++i) {
        lv_row *const row = lv->rows + i;
        --row->cell_count;
        memmove(row->cells + column_index, row->cells + column_index + 1,
                (sizeof *row->cells) * (row->cell_count - column_index));
        row->cells = realloc(row->cells, (sizeof *row->cells) * row->cell_count);
    }
    return 0;
}

listview_free() ought to work if given a null pointer, to be consistent with standard library free(). As currently written, that triggers undefined behaviour.

There's an omission from this function that's revealed when we run the program under Valgrind: we forgot to free each row's cells.

void listview_free(listview *lv)
{
    if (!lv) return;

    // Free rows
    for (int i = 0;  i < lv->rows_count;  ++i) {
        free(lv->rows[i].cells);
    }
    free(lv->rows);

    // Free headers
    free(lv->columns);

    // Free lv itself
    free(lv);
}

We're missing a call to endwin() before we return from main(). That's really irritating, as it leaves an xterm in the alternate screen with echoing disabled...

\$\endgroup\$
4
  • \$\begingroup\$ Thanks for the feedback. I've now added proper error handling to the mallocs and reallocs. However, your suggestion with the "needless allocation" is incorrect. The temporary allocation is necessary because we need to delete an element in the middle of a dynamically allocated array - and hence preserve the part before and the part after. Hence all the two memmove calls. For example, the following unit tests would not work when using your suggestion: pastebin.com/Y1p8y0AU All the other things I agree with. :) \$\endgroup\$
    – 766F6964
    Jun 30, 2021 at 22:07
  • 1
    \$\begingroup\$ Perhaps I wasn't clear with the replacement code: to delete an element from the middle, we simply leave the "before" section untouched, and memmove() the "after" section one position down. If that's still not clear, I'll whip up a full program to demonstrate. (If you applied my change directly to the code, note that I moved the modification of lv->column_count to make the arithmetic simpler, and you'll need to account for that anywhere else it's used, or move it back down - and account for that). \$\endgroup\$ Jul 1, 2021 at 4:54
  • \$\begingroup\$ Well, yeah, maybe an example would help. I find my implementation with two memmove calls much easier to understand. I must be missing something than in your approach, because I can't really get it to work. \$\endgroup\$
    – 766F6964
    Jul 2, 2021 at 21:31
  • 1
    \$\begingroup\$ I've expanded the explanation of that part for you. \$\endgroup\$ Jul 3, 2021 at 7:42

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

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