Forum: PC-Programmierung Interfaces - fixed-width value types


von Vincent H. (vinci)


Lesenswert?

Grüß euch

Mich würde interessieren wie ihr die Parameterübergabe an 
Funktionen/Klassen/etc. in Sprachen handhabt, die über verschiedene 
fixed-width Typen verfügen.

Also also Beispiel bei C/C++ in etwa uint8_t, int16_t, usw. usf.

Nehmen wir an es gibt eine Funktion die irgendeinen jener Typen annimmt 
und damit irgendwas anstellt.
1
void f(T v);

Wie wählt ihr den Übergabe-Parameter aus wenn
- die Funktion ein Byte irgendwohin aussendet
- die Funktion einen 16bit Timer setzt
- die Funktion irgendwas berechnet, der Übergabewert aber etwa nur bis 
16bit skalieren darf

Interessanterweise ertappe ich mich selbst dabei, dass ich vom ersten 
bis zum letzten Beispiel immer weniger strikt mit dem Typen bin. Sprich 
bei einer Funktion die ein Byte sendet würde ich ohne zögern ein 
einzelnes Byte übergeben während ich beim Timer wohl schon nicht mehr so 
sicher wäre...

Die Frage ist, mit welcher Argumentation? Wäre es nicht besser überall 
explizit zu sein und auch beim Timer lediglich 16bit Werte zuzulassen?

Wie handhabt ihr sowas?

von Rolf M. (rmagnus)


Lesenswert?

Ich finde, exakte Größen sollten nur dort eingesetzt werden, wo auch 
zwingend eine benötigt wird, also z.B. bei Hardware-Registern oder einem 
binären Protokoll.
Nur um Wertebereiche einzuschränken, würde ich sie nicht nehmen, schon 
alleine, weil das gar nicht durchgängig klappt. Nehmen wir mal an, dein 
Timer wird im Code fix auf einen Overflow-Wert von 10000 gestellt, um 
mit 10 Mhz betrieben zu werden und immer exakt einmal pro Millisekunde 
überzulaufen. Es gibt aber in der Regel keinen Typ, der exakt bis 9999 
geht.

von Vincent H. (vinci)


Lesenswert?

Rolf M. schrieb:
> Ich finde, exakte Größen sollten nur dort eingesetzt werden, wo auch
> zwingend eine benötigt wird, also z.B. bei Hardware-Registern oder einem
> binären Protokoll.
> Nur um Wertebereiche einzuschränken, würde ich sie nicht nehmen, schon
> alleine, weil das gar nicht durchgängig klappt. Nehmen wir mal an, dein
> Timer wird im Code fix auf einen Overflow-Wert von 10000 gestellt, um
> mit 10 Mhz betrieben zu werden und immer exakt einmal pro Millisekunde
> überzulaufen. Es gibt aber in der Regel keinen Typ, der exakt bis 9999
> geht.

Ja des letzte Beispiel gefällt mir eigentlich auch nicht mehr und war 
unüberlegt. Aber nehmen wir ein anderes, vielleicht ein EEPROM mit 16bit 
Adressraum oder so.
1
eeprom_write(T adress, ...)

Das wäre dann ähnlich zum Timer. Physikalisch wird exakt jenes EEPROM 
immer nur 16bit Adressen schicken, aber sollte sowas Teil der Signatur 
sein oder doch nur Teil der Doku?

Der Vorteil eines uint16_t als Übergabewert besteht daran, dass jeder 
unüberlegte Aufruf sofort ein -Wconversion Warning erzeugt.

Dummerweise besteht der Nachteil eines uint16_t als Übergabewert darin, 
dass jeder Aufruf sofort ein -Wconversion Warning erzeugt. ;)
1
uint8_t b;
2
uint16_t adr;
3
eeprom_write(adr, b);
4
eeprom_write(static_cast<uint16_t>(adr + 1u), b);

Exakte Größen sorgen recht schnell dafür dass der eigene Code mit 
static_casts gespickt ist...

von DPA (Gast)


Lesenswert?

Vincent H. schrieb:
> Aber nehmen wir ein anderes, vielleicht ein EEPROM mit 16bit
> Adressraum oder so.
> eeprom_write(T adress, ...)
>
> Das wäre dann ähnlich zum Timer. Physikalisch wird exakt jenes EEPROM
> immer nur 16bit Adressen schicken, aber sollte sowas Teil der Signatur
> sein oder doch nur Teil der Doku?

Da würde ich size_t nehmen. Fehler würde ich zur Runtime loggen und 
zurückgeben. Und dann eventuell noch eine get_eeprom_size Funktion, je 
nachdem, wofür man das braucht. Bzw. bei Addressen würde ich wohl sogar 
void Pointer nehmen.

So wie ich mich kenne würde ich das vermutlich noch weiter abstrahieren, 
und ein memory Interface mit read write size Funktionen anlegen und im 
eeprom Modul dieses implementieren. Also sowas: (ungetestet)

memory.h
1
struct ns_memory;
2
struct ns_memory_instance;
3
4
struct ns_memory {
5
  const char* type;
6
  ssize_t (*read)(struct ns_memory_instance* memory, void* offset, size_t size, uint8_t out[size]);
7
  ssize_t (*write)(struct ns_memory_instance* memory, void* offset, size_t size, uint8_t in[size]);
8
};
9
10
struct ns_memory_instance {
11
  const struct ns_memory* type;
12
  const char* name;
13
  size_t block_size;
14
};

internal_eeprom.h
1
struct ns_internal_eeprom_memory {
2
  struct ns_memory super;
3
};
4
5
struct ns_internal_eeprom_memory_instance {
6
  struct ns_memory_instance super;
7
};
8
9
extern const struct ns_internal_eeprom_memory_instance ns_internal_eeprom_1;

internal_eeprom.c
1
extern const struct ns_internal_eeprom_memory ns_internal_eeprom_memory;
2
3
static ssize_t read(struct ns_memory_instance* memory, void* offset, size_t size, uint8_t out[size]){
4
  if(!memory || memory->type != &ns_internal_eeprom_memory.super){
5
    errno = EINVAL;
6
    return -1;
7
  }
8
  const struct ns_internal_eeprom_memory_instance* instance = (const struct ns_internal_eeprom_memory_instance*)memory;
9
  ...
10
}
11
12
static ssize_t write(struct ns_memory_instance* memory, void* offset, size_t size, uint8_t in[size]){
13
  ...
14
}
15
16
const struct ns_internal_eeprom_memory ns_internal_eeprom_memory = {
17
  .super = {
18
    .type = "EEPROM",
19
    .read = read,
20
    .write = write
21
  }
22
};
23
24
const struct ns_internal_eeprom_memory_instance ns_internal_eeprom_1 = {
25
  .super = {
26
    .type = &ns_internal_eeprom_memory.super,
27
    .name = "internal1",
28
    .block_size = 1024
29
  }
30
};

Und dann später z.B. so verwenden:
1
const struct ns_memory_instance* bla_memory = &ns_internal_eeprom_1.super;
2
void* bla_location = 0x0001;
3
4
int save_bla(bla* bla){
5
  return bla_memory->type->write(bla_memory, bla_location, sizeof(*bla), bla);
6
}

Vermutlich würde ich mir dann noch eine memory registry/factory 
basierend auf allgemeinen registry/factory funktionen bauen und die 
statischen Memories sich darin selbst registrieren lassen und solches 
zeug. Und bla_memory und bla_location würde ich vermutlich auslagern und 
mit einem anderen Programm generieren lassen, usw. Das heist, falls ich 
nicht gleich ein ganzes Dateisystem mit ähnlicher Abstraktion einbaue... 
"fs_mount("fat", ns_memory_registry_get("EEPROM","internal1"));". Und 
eventuell würde ich dann noch einen memory manager bauen, der Daten auf 
die Memories verteilt abhängig von verfügbarem Platz, Schreibrate, 
Geschwindigkeit, verbleibende Zyklen, dann muss ich auch nichtmehr 
angeben welchen Memory... Ups, ich glaube schreibe schon wieder 
versehentlich ein OS...

(Falls sich jemand wundert, warum ich das in C und nicht in C++ mache: 
Wenn ich das in C schon so Overengineere, stellt euch mal vor, was ich 
in C++ anstellen würde...)

Vincent H. schrieb:
> dass der eigene Code mit static_casts gespickt ist

In C kommt man immerhin noch mit weniger lang auszuschreibenden und nur 
einer Art des Casts zurecht.

von Wilhelm M. (wimalopaan)


Lesenswert?

Vincent H. schrieb:

> Ja des letzte Beispiel gefällt mir eigentlich auch nicht mehr und war
> unüberlegt. Aber nehmen wir ein anderes, vielleicht ein EEPROM mit 16bit
> Adressraum oder so.
>
>
1
> eeprom_write(T adress, ...)
2
>

Die Frage ist doch, ob Adressen vorzeichenlose, Ganzzahlen sind. Das 
sind sie m.E. nicht, denn bspw. eine Multiplikation oder Division oder 
Addition zweier Adressen ist nicht sinnvoll. Das Addieren einer Adresse 
mit einem Offset schon.

Daher schreibe ich für so etwas einen eigenen DT.

Genau aus diesem Grunde hat man ja (endlich) in der C++-stdlib auch den 
DT std::byte eingefügt. Denn ein Byte ist etwas anderes als ein uint8_t.

>
1
> uint8_t b;
2
> uint16_t adr;
3
> eeprom_write(adr, b);
4
> eeprom_write(static_cast<uint16_t>(adr + 1u), b);
5
>
>
> Exakte Größen sorgen recht schnell dafür dass der eigene Code mit
> static_casts gespickt ist...

Nicht, wenn Du Dir domänenspezifische DT nach dem obigen Muster baust.

von Dirk B. (dirkb2)


Lesenswert?

Vincent H. schrieb:
> Ja des letzte Beispiel gefällt mir eigentlich auch nicht mehr und war
> unüberlegt. Aber nehmen wir ein anderes, vielleicht ein EEPROM mit 16bit
> Adressraum oder so.

Und übermorgen hast du ein EEPROM mit 18 Bit Adressraum.
Dann musst du wieder neue Funktionen schreiben.

Jede Einschränkung schränkt ein.

von Vincent H. (vinci)


Lesenswert?

Wilhelm M. schrieb:
> Vincent H. schrieb:
>
>> Ja des letzte Beispiel gefällt mir eigentlich auch nicht mehr und war
>> unüberlegt. Aber nehmen wir ein anderes, vielleicht ein EEPROM mit 16bit
>> Adressraum oder so.
>>
>>
1
>> eeprom_write(T adress, ...)
2
>>
>
> Die Frage ist doch, ob Adressen vorzeichenlose, Ganzzahlen sind. Das
> sind sie m.E. nicht, denn bspw. eine Multiplikation oder Division oder
> Addition zweier Adressen ist nicht sinnvoll. Das Addieren einer Adresse
> mit einem Offset schon.
>
> Daher schreibe ich für so etwas einen eigenen DT.

Schreibst du dann für alles einen Wrapper?
Ich mach das aktuell eher an Stellen wo Funktionsargumente sonst 
unübersichtlich wären (int, int, int...)

Aber ja, mit einem
1
eeprom_write(Address{}, ...)
wär das Problem natürlich umgangen...

von Wilhelm M. (wimalopaan)


Lesenswert?

Vincent H. schrieb:
> Wilhelm M. schrieb:
>> Vincent H. schrieb:
>>
>>> Ja des letzte Beispiel gefällt mir eigentlich auch nicht mehr und war
>>> unüberlegt. Aber nehmen wir ein anderes, vielleicht ein EEPROM mit 16bit
>>> Adressraum oder so.
>>>
>>>
1
>>> eeprom_write(T adress, ...)
2
>>>
>>
>> Die Frage ist doch, ob Adressen vorzeichenlose, Ganzzahlen sind. Das
>> sind sie m.E. nicht, denn bspw. eine Multiplikation oder Division oder
>> Addition zweier Adressen ist nicht sinnvoll. Das Addieren einer Adresse
>> mit einem Offset schon.
>>
>> Daher schreibe ich für so etwas einen eigenen DT.
>
> Schreibst du dann für alles einen Wrapper?

Ja.
Man hat viele Vorteile zur Compilezeit und keine Nachteile zur Laufzeit.

> Ich mach das aktuell eher an Stellen wo Funktionsargumente sonst
> unübersichtlich wären (int, int, int...)

Na, da sowieso. Solche mehrstelligen Funktionen gehen gar nicht.
So etws nennt sich "String Data Types". Gerade das ist ja ein Vorteil 
von C++.

Scott Meyers sagte schon: eine Schnittstelle soll leicht richtig und 
schwer falsch zu benutzen sein.

Die primitiven DT sind ja nur die Grundbausteine einer Sprache. Ohne das 
geht es (fast) nicht. D.h. aber nicht, dass alles ein uint8_t ist ;-) Im 
Gegenteil!

> Aber ja, mit einem
>
1
> eeprom_write(Address{}, ...)
2
>
> wär das Problem natürlich umgangen...

Ja genau.

: Bearbeitet durch User
Bitte melde dich an um einen Beitrag zu schreiben. Anmeldung ist kostenlos und dauert nur eine Minute.
Bestehender Account
Schon ein Account bei Google/GoogleMail? Keine Anmeldung erforderlich!
Mit Google-Account einloggen
Noch kein Account? Hier anmelden.