Beispiellösung

Content

#ifndef ARRAY_HPP
#define ARRAY_HPP

#include <algorithm>
#include <cassert>
#include <cstdlib>
#include <utility>

template<typename T>
class Array {
   public:
      Array() : size(0), data(nullptr) {
      }
      Array(std::size_t size) :
	 size(size), data(size > 0? new T[size]: nullptr) {
      }
      Array(const Array& other) :
	    size(other.size), data(other.data? new T[size]: nullptr) {
	 if (data) {
	    std::copy(other.data, other.data + size, data);
	 }
      }
      friend void swap(Array& a1, Array& a2) {
	 using std::swap;
	 swap(a1.size, a2.size);
	 swap(a1.data, a2.data);
      }
      Array(Array&& other) : Array() {
	 swap(*this, other);
      }
      ~Array() {
	 delete[] data;
      }
      Array& operator=(Array other) {
	 swap(*this, other);
	 return *this;
      }
      std::size_t get_size() const {
	 return size;
      }
      T& operator()(std::size_t index) {
	 assert(index < size);
	 return data[index];
      }
      const T& operator()(std::size_t index) const {
	 assert(index < size);
	 return data[index];
      }
   private:
      std::size_t size;
      T* data;
};

#endif
#include <iostream>
#include "array.hpp"

int main() {
   Array<Array<double>> m(8);
   for (std::size_t i = 0; i < m.get_size(); ++i) {
      m(i) = Array<double>(m.get_size());
      for (std::size_t j = 0; j < m(i).get_size(); ++j) {
	 m(i)(j) = i + j;
      }
   }
   for (std::size_t i = 0; i < m.get_size(); ++i) {
      for (std::size_t j = 0; j < m(i).get_size(); ++j) {
	 std::cout << " " << m(i)(j);
      }
      std::cout << std::endl;
   }
}
theon$ g++ -Wall -o test-array test-array.cpp
theon$ valgrind ./test-array
==1916== Memcheck, a memory error detector
==1916== Copyright (C) 2002-2017, and GNU GPL'd, by Julian Seward et al.
==1916== Using Valgrind-3.13.0 and LibVEX; rerun with -h for copyright info
==1916== Command: ./test-array
==1916== 
 0 1 2 3 4 5 6 7
 1 2 3 4 5 6 7 8
 2 3 4 5 6 7 8 9
 3 4 5 6 7 8 9 10
 4 5 6 7 8 9 10 11
 5 6 7 8 9 10 11 12
 6 7 8 9 10 11 12 13
 7 8 9 10 11 12 13 14
==1916== 
==1916== HEAP SUMMARY:
==1916==     in use at exit: 5,128 bytes in 1 blocks
==1916==   total heap usage: 11 allocs, 10 frees, 78,480 bytes allocated
==1916== 
==1916== LEAK SUMMARY:
==1916==    definitely lost: 0 bytes in 0 blocks
==1916==    indirectly lost: 0 bytes in 0 blocks
==1916==      possibly lost: 5,128 bytes in 1 blocks
==1916==    still reachable: 0 bytes in 0 blocks
==1916==         suppressed: 0 bytes in 0 blocks
==1916== Rerun with --leak-check=full to see details of leaked memory
==1916== 
==1916== For counts of detected and suppressed errors, rerun with: -v
==1916== ERROR SUMMARY: 0 errors from 0 contexts (suppressed: 0 from 0)
theon$ 

Zu den Template-Abhängigkeiten gehören

Interessanterweise wird ein Kopierkonstruktor nicht benötigt, obwohl die Array-Klasse selbst einen anbietet:

#include "array.hpp"

struct Test {
   Test() : i(0) {
   }
   Test(const Test&) = delete;
   int i;
};

int main() {
   Array<Test> t(10);
   Array<Test> t2(t);
}
theon$ g++ -Wall -o test1 test1.cpp
theon$ ./test1
theon$ 

Das liegt daran, dass wir die Elemente nicht kopier-konstruieren, sondern zuerst mit dem default constructor anlegen, bevor dann durch den Kopierkonstruktor die Elemente mit std::copy einzeln einander zugewiesen werden. Wenn wir das vermeiden wollen, müssen wir das Belegen von Speicher von dem Konstruieren trennen.

Die Sache sieht völlig anders aus, wenn wir den Zuweisungsoperator herausnehmen:

#include "array.hpp"

struct Test {
   Test() : i(0) {
   }
   Test& operator=(const Test&) = delete;
   int i;
};

int main() {
   Array<Test> t(10);
   Array<Test> t2(t);
}
theon$ g++ -Wall -o test2 test2.cpp
In file included from /opt/ulm/ballinrobe/include/c++/7.3.0/algorithm:61:0,
                 from array.hpp:4,
                 from test2.cpp:1:
/opt/ulm/ballinrobe/include/c++/7.3.0/bits/stl_algobase.h: In instantiation of 'static _OI std::__copy_move::__copy_m(_II, _II, _OI) [with _II = Test*; _OI = Test*]':
/opt/ulm/ballinrobe/include/c++/7.3.0/bits/stl_algobase.h:386:44:   required from '_OI std::__copy_move_a(_II, _II, _OI) [with bool _IsMove = false; _II = Test*; _OI = Test*]'
/opt/ulm/ballinrobe/include/c++/7.3.0/bits/stl_algobase.h:422:45:   required from '_OI std::__copy_move_a2(_II, _II, _OI) [with bool _IsMove = false; _II = Test*; _OI = Test*]'
/opt/ulm/ballinrobe/include/c++/7.3.0/bits/stl_algobase.h:455:8:   required from '_OI std::copy(_II, _II, _OI) [with _II = Test*; _OI = Test*]'
array.hpp:20:15:   required from 'Array::Array(const Array&) [with T = Test]'
test2.cpp:12:20:   required from here
/opt/ulm/ballinrobe/include/c++/7.3.0/bits/stl_algobase.h:324:18: error: use of deleted function 'Test& Test::operator=(const Test&)'
        *__result = *__first;
        ~~~~~~~~~~^~~~~~~~~~
test2.cpp:6:10: note: declared here
    Test& operator=(const Test&) = delete;
          ^~~~~~~~
theon$ 

Zu den Template-Abhängigkeiten gehört natürlich auch der default constructor. Testen wir mal:

#include "array.hpp"

struct Test {
   Test(int i) : i(i) {
   }
   int i;
};

int main() {
   Array<Test> t;
}
theon$ g++ -Wall -o test3 test3.cpp
theon$ 

Hm... das hat der Übersetzer akzeptiert. Folgendes Beispiel geht jedoch schief:

#include "array.hpp"

struct Test {
   Test(int i) : i(i) {
   }
   int i;
};

int main() {
   Array<Test> t(10);
}
theon$ g++ -Wall -o test4 test4.cpp
In file included from test4.cpp:1:0:
array.hpp: In instantiation of 'Array::Array(std::size_t) [with T = Test; std::size_t = long unsigned int]':
test4.cpp:10:20:   required from here
array.hpp:15:30: error: no matching function for call to 'Test::Test()'
   size(size), data(size > 0? new T[size]: nullptr) {
                              ^~~~~~~~~~~
test4.cpp:4:4: note: candidate: Test::Test(int)
    Test(int i) : i(i) {
    ^~~~
test4.cpp:4:4: note:   candidate expects 1 argument, 0 provided
test4.cpp:3:8: note: candidate: constexpr Test::Test(const Test&)
 struct Test {
        ^~~~
test4.cpp:3:8: note:   candidate expects 1 argument, 0 provided
test4.cpp:3:8: note: candidate: constexpr Test::Test(Test&&)
test4.cpp:3:8: note:   candidate expects 1 argument, 0 provided
theon$ 

Warum? Der Punkt ist der, dass der Übersetzer tatsächlich sich beim Instantiieren nur auf die Code-Teile beschränkt, die tatsächlich verwendet werden. Bei test3.cpp wurde nur der default constructor verwendet, der den new-Operator nicht verwendet. Der reguläre Konstruktor kam nicht zum Einsatz. Deswegen kam es nicht zu einem Fehler, obwohl eine Template-Abhängigkeit nicht erfüllt war. Erst bei test4.cpp kam der reguläre Konstruktor zum Einsatz und somit blieb dem Übersetzer hier nichts anderes übrig, als eine Fehlermeldung zu generieren.

Andere Übersetzer verhalten sich hier ähnlich. Hier das gleiche Beispiel mit dem Oracle-Studio-Übersetzer:

theon$ CC -std=c++11 -o test3 test3.cpp
theon$ CC -std=c++11 -o test4 test4.cpp
"array.hpp", line 15: Error: Can't find Test::Test() to initialize a vector.
"test4.cpp", line 10:     Where: While instantiating "Array::Array(unsigned)".
"test4.cpp", line 10:     Where: Instantiated from non-template code.
1 Error(s) detected.
theon$