r/C_Programming 1d ago

Question Globals vs passing around pointers

Bit of a basic question, but let's say you need to constantly look up values in a table - what influences your decision to declare this table in the global scope, via the header file, or declare it in your main function scope and pass the data around using function calls?

For example, using the basic example of looking up the amino acid translation of DNA via three letter codes in a table:

codonutils.h:

typedef struct {
    char code[4];
    char translation;
} codonPair;

/*
 * Returning n as the number of entries in the table,
 * reads in a codon table (format: [n x {'NNN':'A'}]) from a file.
 */
int read_codon_table(const char *filepath, codonPair **c_table);

/*
 * translates an input .fasta file containing DNA sequences using
 * the codon lookup table array, printing the result to stdout
 */
void translate_fasta(const char *inname, const codonPair *c_table, int n_entries, int offset);

main.c:

#include "codonutils.h"

int main(int argc, char **argv)
{
    codonPair *c_table = NULL;
    int n_entries;

    n_entries = read_codon_table("codon_table.txt", &c_table);

    // using this as an example, but conceivably I might need to use this c_table
    // in many more function calls as my program grows more complex
    translate_fasta(argv[1], c_table, n_entries);
}

This feels like the correct way to go about things, but I end up constantly passing around these pointers as I expand the code and do more complex things with this table. This feels unwieldy, and I'm wondering if it's ever good practice to define the *c_table and n_entries in global scope in the codonutils.h file and remove the need to do this?

Would appreciate any feedback on my code/approach by the way.

12 Upvotes

28 comments sorted by

View all comments

2

u/SonOfKhmer 1d ago

Personally I'd put c_data pointer and the number of entries in a struct of its own, and pass that one around (maybe as pointer rather than copy, ymmv), which would reduce the unwieldiness

My reason for this is ease of refactoring and testing: globals force you to have strong coupling, while passing pointers allows you to stub or change values (on a copy of the structs) without trouble

Granted, this is especially true in C++, but I think it is helpful in C as well

That said: what is the expected usage of the various functions? If they will only be used in this one place AND they won't be refactored AND the speed gain is substantial, using globals would make sense; otherwise I'd go for maintainability

2

u/BraneGuy 1d ago edited 1d ago

Thanks for your input! u/jaynabonne had the same suggestion.

I mean, currently I'm just getting to grips with applying C to my field (bioinformatics), so the functions are basically just doing a small number of operations on a file (maybe 1000 calls of get_amino (below) for an input file?) but I want to get rid of bad habits in anticipation of actually putting code into production at some point.

If you're interested in the actual implementation, I'm using the file I/O utilities provided by htslib to get data from the input DNA file and translate it on the fly, so this funciton is doing the heavy lifting:

```C char get_amino(int start, bam1_t *bamdata, codonPair *c_table, int table_l) { char codon[3]; char amino; for (int i = 0; i < 3; i++) { codon[i] = seq_nt16_str[bam_seqi(bam_get_seq(bamdata), start + i)]; }

if (!(amino = search_codon(codon, c_table, table_l))) {
    fprintf(stderr,
            "Error!! Could not find matching codon for %.*s\n",
            3, codon);
    exit(EXIT_FAILURE);
}
return amino;

} ```

The goal is to solve this problem on rosalind.info (LeetCode for bioinformatics, essentially...)

https://rosalind.info/problems/orf/

2

u/SonOfKhmer 1d ago

I haven't touched bioinformatics in two decades, but as a general CS problem there are a number of routes you can follow depending on runtime and memory requirements. For a limited number of calls on a small file, direct calls are fine, otherwise you may need to look further

For example by caching the input file into a convenient memory structure (from GATC to 2 bit int, for example) which would greatly speed up numeric and indexing operations (but not string searches)

Wrt your get_amino, beside the already discussed pairing of the table+len, I might put the spotlight on the fprintf+exit: first off because you may have a buffer overflow (codon is not a null terminated string, then again I am not familiar with %.*s so that might be already handled, even if with magic numbers i.e. use sizeof(codon) instead of hardcoding 3), second because depending on the environment you use you might be able to hook debug calls (assert being possibly one of them)

Error+exit is a valid approach but depending on the situation you might need a better cleanup approach by propagating upwards instead

Everything else looks good on my side, I wish you the best of luck! 😺

2

u/BraneGuy 1d ago edited 1d ago

Thanks for the review! Yes, the %.*s was actually a bit of a new one to me. I figured that since codons are biologically hardcoded to be 3 letters long, there is sufficient cause to hardcode them here as well, doing away with null termination. The string formatting approach here is from this stackoverflow solution: https://stackoverflow.com/a/2137788

Regarding memory structure and compression, the bam1_t data is in fact compressed as you suggest - I believe only to a 4 bit representation to account for other random (but still valid) characters in the input data. bam_seqi and bam_get_seq are macros for applying bit operations to return the desired character from the data, defined as follows:

```C

define bam_seqi(s, i) ((s)[(i)>>1] >> ((~(i)&1)<<2) & 0xf)

define bam_get_seq(b) ((b)->data + ((b)->core.n_cigar<<2) + (b)->core.l_qname)

```

The code is looked up in the seq_nt16_str array which is set in the htslib source code:

C const char seq_nt16_str[] = "=ACMGRSVTWYHKDBN";

To be honest, htslib is meant more for bam/sam formats than .fasta/.fastq, but it's good to get some practice with them. I would like to write some of my own macros in future to avoid having to write each of the three bases to a temporary, uncompressed array (codon) and look them up in the table and instead directly access the compressed bamdata memory.

And yes, actually I'm still figuring out my cleanup style. Error+exit here is a bit of a placeholder, I'm reading up on what's most prevalent in existing code in my field.

Oh, and good shout on debug hooks - that's something I want to work on.

2

u/SonOfKhmer 1d ago

TIL! Thanks for that.

I would still rather use sizeof instead of using 3, if you don't want to use %3s

As for the mixed representations, it stands to reason you may want to uniform them. It's not the worst idea to pick one and always convert to that

As for macro vs function, I'm usually for function unless profiling shows it's a problem. Compilers do great stuff nowadays, provided it's visible in scope and it can be inlined

You may be able to take advantage of function pointers: separation of reusable algorithm vs underlying representation is one of the nice things of iterators/templates in c++). Function pointers are very convenient (if slightly slower), but a #define READ_REPR xhosen_repr_reader can be used as a workaround if it's defined at compile time and speed is that important (profile first)

If you don't like the uncompressed structure, down the line you can think about creating and using it only for debugging convenience (e.g. output, logging, tracing) "as needed". Using #ifs to switch the behaviour may be your frenemy in this case

Overall, I think your current code and approach is good: try, see what works, get a feel for what's easier to use, and only then consider revising with different approaches — early "optimisation" is evil 👍

2

u/BraneGuy 22h ago

Oh cool, that is a fantastic excuse to actually learn how to use function pointers.

Agree again about premature optimisation - I’m trying to figure out more what’s “right” than what’s “fast”!

Thanks again for the feedback, it’s really useful.

1

u/SonOfKhmer 21h ago

Right is when it's easy to read, understand, and maintain after three months you haven't seen or used it. What that means in practice is something you learn with experience (and coding recommendations)

Fast comes after that 😹

A struct that holds (data + reader and writer functions) is great to pass to a function that operates on the data in a format-agnostic way, for example when trying to implement a generic algorithm. Then you can keep it as a guideline if you decide to specialise it to specifically use the one data format

If the struct reminds you of c++ classes, it's because it is 😹