When Immutability and Nullability Meet: Exploring Mutable Objects in C

Liste des GroupesRevenir à cl c  
Sujet : When Immutability and Nullability Meet: Exploring Mutable Objects in C
De : thiago.adams (at) *nospam* gmail.com (Thiago Adams)
Groupes : comp.lang.c
Date : 17. Sep 2024, 23:27:09
Autres entêtes
Organisation : A noiseless patient Spider
Message-ID : <vccs7e$3mu1n$1@dont-email.me>
User-Agent : Mozilla Thunderbird
I found some use cases where we may want disable const and _not_null (if C had this qualifier) at same time. Kind of "anti-qualifier".
For the impatient go direct to "mutable qualifier allows certain exceptions to the usual contract of immutability"
Nullable Pointers
The concept of nullable pointers is introduced to refine the type system by explicitly indicating when pointers can or cannot be null.
Take, for instance, the standard function strdup:
char * strdup(const char * src);
In this function, the argument src must reference a valid string. The function returns a pointer to a newly allocated string, or a null pointer if an error occurs.
In Cake, the _Opt qualifier extends the type system by marking pointers that can be null. Only pointers qualified with _Opt are explicitly nullable, providing better clarity about which pointers may need null checks.
The _Opt qualifier is placed similarly to const, after the * symbol. For example, the declaration of strdup in Cake would look like this:
char * _Opt strdup(const char * src);
Static analysis tools need to know when these new rules for nullable pointers apply, particularly for unqualified pointers. This is managed through the #pragma nullable enable directive, which informs the compiler when to enforce these rules.
Example 1: Warning for Non-Nullable Pointers
#pragma nullable enable
int main(){
   int * p = nullptr; // warning
}
In this example, a warning is generated because p is non-nullable, yet it is being assigned nullptr.
Example 2: Converting Non-Nullable to Nullable
The conversion from a non-nullable pointer to a nullable one is allowed, as shown below:
#pragma nullable enable
char * get_name();
int main(){
   char * _Opt s = get_name();
}
Here, the return value of get_name() is non-nullable by default, but it is assigned to a nullable pointer s, which does not trigger any warnings.
Example 3: Diagnostic for Nullable to Non-Nullable Conversion
Consider the following case:
#pragma nullable enable
char * _Opt strdup(const char * src);
void f(char *s);
int main()
{
    char * _Opt s1 = strdup("a");
    f(s1); // warning
}
In this scenario, s1 is declared as nullable, but f expects a non-nullable argument. This triggers a warning, as the nullable pointer s1 could potentially be null when passed to f. To resolve this warning, a null check is required:
   if (s1)
     f(s1); // ok
This warning relies on flow analysis, which ensures that the potential nullability of pointers is checked before being passed to functions or assigned to non-nullable variables.
Non nullable members
The concept of nullable types is present in some language like C# and Typescript. Both languages have the concept of constructor for objects. So, for objects members, the compiler checks if after the constructor the non-nullable members have being assigned to a non null value.
The other way to see this, is that during construction the non nullable pointer member can be null, before they receive a value.
In C, we don t have the concept of constructor, so the same approach cannot be applied directly.
Cake, have a mechanism using the qualifier _Opt before struct types to make all non-nullable members as nullable for a particular instance.
struct X {
   char * name; //non nullable
};
struct X * _Opt makeX(const char* name)
{
   _Opt struct X * p = calloc(1, sizeof * p);
   if (p == NULL)
     return NULL;
   char * _Opt temp = strdup(name);
   if (temp == NULL)
     return NULL;
   x->name = temp;
   return x;
}
Just like in C# or Typescript, we cannot leave this function with a nullable member being null. But the particular instance of p is allowed to have nullable members.
This is also useful to accept for some functions like destructor, partially constructed object.
void x_destroy(_Opt struct X * p)
{
    free(p->name); //ok
}
Note that this concept also could be applied for const members.
The introduction of a mutable qualifier allows certain exceptions to the usual contract of immutability and non-nullability during transitional phases, such as in constructors and destructors. This means that objects marked as mutable can temporarily violate their normal constraints, such as modifying const members or assigning null to non-nullable pointers during these phases.
Consider the following code example:
struct X {
   const char * const name; // non-nullable
};
struct X * _Opt makeX(const char* name)
{
   mutable struct X * p = calloc(1, sizeof *p);
   if (p == NULL)
     return NULL;
   char * _Opt temp = strdup(name);
   if (temp == NULL)
     return NULL;
   p->name = temp;  // OK!!
   return p;
}
In this example, struct X has a const member name, which is non-nullable. Under normal conditions, modifying a const member after initialization would be disallowed. However, the mutable qualifier temporarily relaxes this rule during the object’s creation process, allowing modifications even to const members, and allowing a non-nullable pointer to be null before the object’s initialization completes.
We also have an implicit contract for struct members. Generally, we assume that members are initialized, but we lack a qualifier to explicitly indicate "initialized member." For instance, when using malloc, members are initially uninitialized, but they should receive a value before being used.
struct X * _Opt makeX(const char* name)
{
   mutable struct X * p = malloc(sizeof *p);
   if (p == NULL)
     return NULL;
   char * _Opt temp = strdup(name);
   if (temp == NULL)
     return NULL;
   p->name = temp;  // OK!! name fixed
   return p;
}
Transitional State:
During the object creation (or destruction), the instance is considered to be in a transitional state, where the usual constraints—such as non-nullable pointers and immutability—are lifted. For example, in the makeX function, p->name can be set to temp, even though name is const. This allows flexibility during initialization, after which the object is returned to its normal state with the contract fully enforced.
Effect on the Final Object:
Once the transitional phase is over and the object is returned, the contract that governs the object (such as immutability of name and non-nullability of pointers) is fully reinstated. The mutable qualifier only applies within the scope of the constructor or destructor, ensuring that once the object is fully constructed, its state is valid and consistent with the type system’s rules.
This approach allows for more flexibility during object creation while maintaining strong contracts once the object is finalized, enhancing both safety and expressiveness in the code.
OBS: mutable qualifier is not yet implemented in Cake. However, _Opt for structs is implemented.

Date Sujet#  Auteur
17 Sep23:27 * When Immutability and Nullability Meet: Exploring Mutable Objects in C2Thiago Adams
18 Sep22:20 `- Re: When Immutability and Nullability Meet: Exploring Mutable Objects in C1Thiago Adams

Haut de la page

Les messages affichés proviennent d'usenet.

NewsPortal