📜 ⬆️ ⬇️

Comparing D and C ++ and Rust with examples

This post is based on Rust and C ++ Comparison on examples and complements the examples given there with D code with a description of the differences.

All examples were compiled using the DMD v2.065 x86_64 compiler.

Check Template Types



Templates in Rust are checked for correctness prior to their instantiation, so there is a clear separation between errors in the template itself (which should not be if you use someone else's / library template) and in the place of instantiation, where all that is required of you is to meet the requirements for type described in the template:
trait Sortable {} fn sort<T: Sortable>(array: &mut [T]) {} fn main() { sort(&mut [1,2,3]); } 


')
In D, a different approach is used: guard can be hung on the templates, functions, structures, which will not allow the function to be included in the overload set if the template parameter does not have a specific property.
 import std.traits; // auto sort(T)(T[] array) {} -   guard  auto sort(T)(T[] array) if(isFloatingPoint!T) {} void main() { sort([1,2,3]); } 


The compiler will complain as follows:
source / main.d (27): Error: template main.sort cannot deduce function from argument types! () (int []), candidates are:
source / main.d (23): main.sort (T) (T [] array) if (isFloatingPoint! T)


However, you can get almost identical “resolving” behavior of Rust as follows:
 template Sortable(T) { // ,   ,    swap    enum Sortable = __traits(compiles, swap(T.init, T.init)); //       static assert(Sortable, "Sortable isn't implemented for "~T.stringof~". swap function isn't defined."); } auto sort(T)(T[] array) if(Sortable!T) {} void main() { sort([1,2,3]); } 

Compiler output:
source / main.d (41): Error: static assert “Sortable is not implemented for int. swap function not defined. "
source / main.d (44): instantiated from here: Sortable! int
source / main.d (48): instantiated from here: sort! ()


The ability to display your error messages makes it possible in almost all cases to avoid the kilometer-long logs of the compiler about problems with templates, but the price of such freedom is high - you have to think over the limits of applicability of your templates and write clear (!) Messages with your hands. Given that the template parameter T can be: type, lambda, another template (template template, etc., it allows you to simulate depended types), expression, list of expressions - often only a certain subset of perverse user fantasies of errors is processed.

Access to remote memory


In D, the default is GC , which itself performs reference counting and deletes unnecessary objects. Also in D there is a separation - the release of the object's resources and the removal of the object. In the first case, use destroy () , in the second GC.free . You can allocate memory managed by GC - GC.malloc . Then the program itself will free up memory during GC startup, if a piece of memory is unreachable through links / pointers.

It is also possible to allocate memory through the C-shny malloc family of functions:
 import std.c.stdlib; void main() { auto x = cast(int*)malloc(int.sizeof); //       scope scope(exit) free(x); //       free(x); *x = 0; } 

*** Error in `demo ': double free or corruption (fasttop): 0x0000000001b02650 ***


D allows you to program at different levels, up to the inline assembler. We refuse GC - we take responsibility for the class of errors: leaks, access to remote memory. The use of RAII (scope of expression in the example) can significantly reduce the headache with this approach.

In the recently published book D Cookbook there are chapters devoted to the development of custom arrays with manual memory management and writing a kernel module on D (without GC and without runtime). The standard library does become practically useless with the complete abandonment of runtime and GC, but it was originally designed for using their features. The place of the embedded-style library is still not occupied by anyone.

Lost pointer to local variable


Rust version:
 fn bar<'a>(p: &'a int) -> &'a int { return p; } fn foo(n: int) -> &int { bar(&n) } fn main() { let p1 = foo(1); let p2 = foo(2); println!("{}, {}", *p1, *p2); } 



Analog on D (almost repeats the example in C ++ from a source post):
 import std.stdio; int* bar(int* p) { return p; } int* foo(int n) { return bar(&n); } void main() { int* p1 = foo(1); int* p2 = foo(2); writeln(*p1, ",", *p2); } 

Conclusion:
2.2


Rust in this example has an advantage, I do not know of any such language in which such a powerful analyzer of the lifetime of variables was embedded. The only thing I can say in defense D is that in safe mode the compiler does not compile the previous code:
Error: cannot be taken function safe


Also in 90% of the code, the D pointers are not used (low level - high responsibility), for most cases ref is suitable:
 import std.stdio; ref int bar(ref int p) { return p; } ref int foo(int n) { return bar(n); } void main() { auto p1 = foo(1); auto p2 = foo(2); writeln(p1, ",", p2); } 

Conclusion:
1.2


Uninitialized variables


C ++
 #include <stdio.h> int minval(int *A, int n) { int currmin; for (int i=0; i<n; i++) if (A[i] < currmin) currmin = A[i]; return currmin; } int main() { int A[] = {1,2,3}; int min = minval(A,3); printf("%d\n", min); } 



In D, all default values ​​are initialized with T.init, but it is possible to indicate to the compiler that, in a particular case, initialization is not required:
 import std.stdio; int minval(int[] A) { int currmin = void; // undefined behavior foreach(a; A) if (a < currmin) currmin = a; return currmin; } void main() { auto A = [1,2,3]; int min = minval(A); writeln(min); } 


A positive point: to shoot in the foot you need to specifically want this. Accidentally uninitializing a variable in D is almost impossible (maybe using the copy-paste method).

A more idiomatic (and working) version of this function would look like this:
 fn minval(A: &[int]) -> int { A.iter().fold(A[0], |u,&a| { if a<u {a} else {u} }) } 



For comparison, the variant on D:
 int minval(int[] A) { return A.reduce!"a < b ? a : b"; //  //return A.reduce!((a,b) => a < b ? a : b); } 


Implicit copy constructor


C ++
 struct A{ int *x; A(int v): x(new int(v)) {} ~A() {delete x;} }; int main() { A a(1), b=a; } 



Similar version on D:
 struct A { int *x; this(int v) { x = new int; *x = v; } } void main() { auto a = A(1); auto b = a; *bx = 5; assert(*ax == 1); // fails } 


In D, structures support only copying semantics, and also do not have an inheritance mechanism (replaced by impurities), virtual functions, and other features of objects. The structure is just a piece of memory, the compiler does not add anything extra. For the correct implementation of the example, you need to define a postblit constructor (almost a copy constructor):
  this(this) //        this { //        auto newx = new int; *newx = *x; x = newx; } 


There is a clear separation: if the object needs copying semantics — like simple types of type int — structures are used. If passing by reference, then classes are used. In the book Alexandrescu (there is a translation ) all these points are covered.

Rust will not do anything behind your back. Want an automatic implementation of Eq or Clone? Simply add the deriving property to your structure:
 #[deriving(Clone, Eq, Hash, PartialEq, PartialOrd, Ord, Show)] struct A{ x: Box<int> } 


There is no analog of this mechanism in D. For structures, all such operations are overloaded via structual typing (often confused with duck typing), if the structure has a suitable method, then it is used, if not, the default implementation.

Overlapping memory


 #include <stdio.h> struct X { int a, b; }; void swap_from(X& x, const X& y) { xa = yb; xb = ya; } int main() { X x = {1,2}; swap_from(x,x); printf("%d,%d\n", xa, xb); } 


Gives us:
2.2



Similar D code that also does not work:
 struct X { int a, b; } void swap_from(ref X x, const ref X y) { xa = yb; xb = ya; } void main() { auto x = X(1,2); swap_from(x, x); writeln(xa, ",", xb); } 


Rust in this case definitely wins. I did not find a way to detect memory overlapping at compile time on D.

Spoiled iterator


In D, the iterator abstraction is replaced by Ranges , we try to change the container during the passage:
 import std.stdio; void main() { int[] v; v ~= 1; v ~= 2; foreach(val; v) { if(val < 5) { v ~= 5 - val; } } writeln(v); } 

Conclusion:
[1, 2, 4, 3]


If you change the range array, the previously received does not change, until the end of the foreach block, this range will point to the data of the “old” array. You may notice that all changes occur in the tail of the array, you can complicate the example and add to the beginning and to the end at the same time:

 import std.stdio; import std.container; void main() { DList!int v; v.insert(1); v.insert(2); foreach(val; v[]) //  []  range { if(val < 5) { v.insertFront(5 - val); v.insertBack(5 - val); } } writeln(v[]); } 

Conclusion:
[3, 4, 1, 2, 4, 3]


In this case, a doubly linked list from the standard library was used. When using an array, adding to its beginning always leads to its re-creation, but it does not break the algorithm, the old range indicates the old array, and we work with new copies of the array, and thanks to GC we can not worry about stubs in memory. And in the case of the list, it is not necessary to re-allocate the entire memory, only for new items.

Dangerous Switch


 #include <stdio.h> enum {RED, BLUE, GRAY, UNKNOWN} color = GRAY; int main() { int x; switch(color) { case GRAY: x=1; case RED: case BLUE: x=2; } printf("%d", x); } 

Gives us a "2". In Rust zhy you are obliged to list all the options when comparing with the sample. In addition, the code does not automatically jump to the next option unless it encounters a break.


In D, the switch may have the final keyword; the compiler will force you to write all the matching variants. In the absence of a final, a default block is required. Also in recent versions of the compiler, the implicit “failing” to the next label is marked as deprecated, an explicit goto case is required. Example:
 import std.stdio; enum Color {RED, BLUE, GRAY, UNKNOWN} Color color = Color.GRAY; void main() { int x; final switch(color) { case Color.GRAY: x = 1; case Color.RED: case Color.BLUE: x = 2; } writeln(x); } 

Compiler output:
source / main.d (227): Error: enum member UNKNOWN is not represented in final switch
source / main.d (229): Warning: switch case fallthrough - use 'goto case;' if intended
source / main.d (229): Warning: switch case fallthrough - use 'goto case;' if intended


Random semicolon


 int main() { int pixels = 1; for (int j=0; j<5; j++); pixels++; } 


In Rust, you must enclose the bodies of cycles and comparisons in braces. A trifle, of course, but less a class error.


In D, the compiler will issue a warning (by default, warnings are errors) and suggest replacing; on {}.

Multithreading


 #include <stdio.h> #include <pthread.h> #include <unistd.h> class Resource { int *value; public: Resource(): value(NULL) {} ~Resource() {delete value;} int *acquire() { if (!value) { value = new int(0); } return value; } }; void* function(void *param) { int *value = ((Resource*)param)->acquire(); printf("resource: %p\n", (void*)value); return value; } int main() { Resource res; for (int i=0; i<5; ++i) { pthread_t pt; pthread_create(&pt, NULL, function, &res); } //sleep(10); printf("done\n"); } 


It spawns several resources instead of one:
done
resource: 0x7f229c0008c0
resource: 0x7f22840008c0
resource: 0x7f228c0008c0
resource: 0x7f22940008c0
resource: 0x7f227c0008c0



In D, similarly to Rust, the compiler checks access to shared resources. By default, all memory is indivisible, each thread works with its own copy of the environment (which is stored in TLS ), and all shared resources are marked with the keyword shared. Let's try to write on D:
 import std.concurrency; import std.stdio; class Resource { private int* value; int* acquire() { if(!value) { value = new int; } return value; } } void foo(shared Resource res) { // Error: non-shared method main.Resource.acquire is not callable using a shared object writeln("resource ", res.acquire); } void main() { auto res = new shared Resource(); foreach(i; 0..5) { spawn(&foo, res); } writeln("done"); } 


The compiler did not see explicit synchronization and did not allow compiling code with potential race condition. In D, there are many synchronization primitives, but for simplicity, consider a Java-like monitor mutex for objects:
 synchronized class Resource { private int* value; shared(int*) acquire() { if(!value) { value = new int; } return value; } } 


Conclusion:
done
resource 7FDED3805FF0
resource 7FDED3805FF0
resource 7FDED3805FF0
resource 7FDED3805FF0
resource 7FDED3805FF0


Each time you call acquire, the object's monitor is captured by the stream and all other threads are blocked until the resource is released. Pay attention to the return type of the acquire function, in D such modifiers as shared, const, immutable are transitive, if they mark a reference to a class, then all fields and returned pointers to fields are also marked with a modifier.

A bit about insecure code


Unlike Rust, all code in D by default is @ system, i.e. unsafe. The code marked @ safe restricts the programmer and does not allow to play with pointers, assembler inserts, unsafe type conversions and other dangerous features. To use unsafe code, the @ trusted modifier in the safe code is key places that must be carefully covered with tests.

Comparing with Rust, I very much wish such a powerful system for analyzing the lifetime of references for D. The “cultural” exchange between these languages ​​will only benefit them.

Thanks to ReklatsMasters for additional material on GC and structures.

Source: https://habr.com/ru/post/225507/


All Articles