C++Guns – RoboBlog

23.03.2018

C++ Guns: concept: force function return type

Filed under: Allgemein — Tags: — Thomas @ 02:03

Das Problem von impliziten Konversionen in C/C++ ist nicht neu. Wenn integer in floats und floats in integer kopiert werden und dabei die Werte der Zahlen verändert werden, kann das in wissenschaftlichen Anwendungen katastrophale Auswirkungen haben. Es müssen verschiedene Szenarien beachtet werden. So ist z.b. das kopieren eines 32bit integer in ein 64bit float kein Problem. Hingegen kann ein 32bit integer nicht ohne Verlust in ein 8bit integer gespeichert werden. Außer, es ist vom Algorithmus her unmöglich, dass sich die Wertebereiche überlappen.

Ich werde versuchen das Thema an Hand von Beispielen zu verdeutlichen. Sowohl für Funktions-Parameter als auch für Rückgabe Typen. Diverse Compilerparameter die man immer nutzen sollte, sowie ob Concepts hier helfen können.

Beispiel 1:
Drei Member Funktionen die jeweils ein bool übergeben, und bool zurück geben sollen. Aber der Programmierer hat sich vertan und übergibt einmal ein integer und float. Im zweiten Fall wird ein Integer zurück gegeben und im dritten Fall ist der Return Type falsch gewählt, aber es wird dennoch ein bool zurück gegeben.

Da eine implizite Umwandlung zwischen bool und integer/float erlaubt ist, erwarte ich nicht, dass der Compiler hier Fehlermeldungen wirft. Möglicherweise nur ein paar Warnings. Compiliert wird mit -Wall -Wextra -Wconversion

#include <cassert>
  
bool b_good(bool b) { 
  assert(b);
  return true;     
};

bool b_bad(bool b) { 
  assert(b);
  return 2;     
};

float b_bad2(bool b) {
  assert(b);
  return true; 
};

int main() {
  assert(b_good(true)); // okay
  assert(b_bad(4));     // bad but works. pass int ->   bool return int -> bool -> bool
  assert(b_bad2(2.0));  // bad but works. pass float -> bool return bool-> float -> bool
}

Der Compiler schweigt und das Programm läuft sauber durch. Salopp gesagt, alles was nicht 0 ist, ist wahr. Das funktioniert, aber fördert nicht die Fehlerfreiheit des Programms.

Mit narrowing conversion / brace initialization / list initialization, mit den geschweiften Klammern gibt es immerhin Warnungen bzw. gleich ein Fehler für Konstanten:

assert(b_bad({4})); 
...
return {2};

error: narrowing conversion of ‘4’ from ‘int’ to ‘bool’ inside { } [-Wnarrowing]
assert(b_bad({4}));

error: narrowing conversion of ‘2’ from ‘int’ to ‘bool’ inside { } [-Wnarrowing]
return {2};

Aber überall jetzt noch mehr Klammern einbauen kann nicht die Lösung sein. Beim return kann ich es mir noch vorstellen, aber nicht bei jedem Funktionsparameter.

Wie sieht es aus wenn statt dem Typ bool das Konzept Boolean genutzt wird? Nun, meine Variante dieses Konzept ist nicht wirklich eins, es gleicht mehr einem constraint. Ein Konzept soll ja auch möglichst vielen constrain bestehen. Aber es funktioniert. Ersetzt man die Funktionsparameter Typen durch das Konzept Boolean, kommen gute Fehlermeldungen, wie gewünscht.

template <class B> concept bool Boolean = std::is_same<B, bool>::value;

error: cannot call function ‘auto b_bad(auto:2) [with auto:2 = int]’
note: constraints not satisfied
Boolean b_bad(Boolean b) {
note: within ‘template concept const bool Boolean [with B = int]’
template concept bool Boolean = std::is_same::value;
‘std::is_same::value’ evaluated to false

error: cannot call function ‘float b_bad2(auto:3) [with auto:3 = double]’
note: constraints not satisfied
float b_bad2(Boolean b) {
note: within ‘template concept const bool Boolean [with B = double]’
template concept bool Boolean = std::is_same::value;
note: ‘std::is_same::value’ evaluated to false

#include <type_traits>

// Forcing a concept to be a fixed type. Is this a good concept or misuse?
template<typename B>
concept bool BoolType = std::is_same<B,bool>::value;

template< typename ObjectType>
concept bool ObjectInterface = requires (ObjectType obj, int a) {
  // Compound Requirements

  // does not work. 
  // 4) trailing-return-type that names a type that does not use placeholders: 
  // 4a) the type named by trailing-return-type is valid (type constraint)
  // 4b) the result of the expression is implicitly convertible to that type (implicit conversion constraint)
  // {obj.u1(a)} -> bool;  
  
  // works
  // 3) trailing-return-type that names a type that uses placeholders,
  // the type must be deducible from the type of the expression (argument deduction constraint)  
  {obj.u1(a)} -> BoolType;
};

void f(ObjectInterface obj) {
  obj.u1(1);
}

struct MyType {
  auto u1(int a) { return false; }
//  auto u1(int a) { return 1; } // error
};

int main () {
  f(MyType());
}

concept.cpp: In function ‘int main()’:
concept.cpp:30:13: error: cannot call function ‘void f(auto:1) [with auto:1 = MyType]’
f(MyType());
^
concept.cpp:21:6: note: constraints not satisfied
void f(ObjectInterface obj) {
^
concept.cpp:8:14: note: within ‘template concept const bool ObjectInterface [with ObjectType = MyType]’
concept bool ObjectInterface = requires (ObjectType obj, int a) {
^~~~~~~~~~~~~~~
concept.cpp:8:14: note: with ‘MyType obj’
concept.cpp:8:14: note: with ‘int a’
concept.cpp:8:14: note: unable to deduce placeholder type ‘BoolType’ from ‘obj.MyType::u1(a)’

No Comments

No comments yet.

RSS feed for comments on this post.

Sorry, the comment form is closed at this time.

Powered by WordPress