O nadużywaniu C++/CLI

19

@Azarien napisał dzisiaj, że Nie ma powodu, by C++/CLI musiało być tylko do „portowania bibliotek” poza tym, że wielu tak uważa. Chciałbym poruszyć ten wątek, a że w tamtym w gruncie rzeczy mowa o innym języku to założyłem nowy.

Ostatnimi czasy widać w Polsce zwiększające się zainteresowanie językiem C++/CLI. Jak dla mnie, wynika to z niczego innego jak próbą pójścia na skróty w nauce czystego C++. Wiadomo: czytelniejsza biblioteka standardowa, a klepanie interfejsu użytkownika w Visual Studio nigdy nie było prostsze. Koniec końców najczęściej jest taki, że przychodzi człowiek na forum, bo nie może sobie poradzić ze składnią tego tworu i okazuje się, że nie poznał ani C++, ani .NET.

Także apeluję do młodszych (stażem, bo sam młody wiekiem jestem) programistów: nie zabierajcie się za C++/CLI, jeżeli chcecie się nauczyć C++ oraz nie zabierajcie się za C++/CLI, jeżeli chcecie nauczyć się .NET.

W pierwszym przypadku uczcie się po prostu C++. Po dogłębnym przestudiowaniu wcale niedużej biblioteki standardowej ze zdziwieniem stwierdzicie na koniec, że i tak zaoszczędzi wam kupę czasu, bo przykładowo nie trzeba już korzystać z ręcznie klepanych pętli czy mieszać gdzieś w kod bibliotekę C do operowania na napisach, bo nigdy wcześniej nie spodziewaliście się, że nagłówek <algorithm>, iteratory, insertery, i/o stringstreamy mogą być tak proste i przydatne. Można pójść dalej i poznać nowe (w standardzie) inteligentne wskaźniki i na zawsze zrezygnować z ręcznego zarządzania pamięci.

Jeżeli z kolei podoba wam się przeciąganie kontrolek na formę w Visual Studio i nie macie odgórnego wymogu (ze szkoły, uczelni) na naukę C++ to bezwzględnie piszcie od razu w C#. O ile na samiutkim początku może zbyt dużej różnicy nie ma, bo kompilator i tak wygeneruje kod za was, to już za chwilę zababracie się zupełnie w C++/CLI. Nie jest to wcale dziwne, bo nie dość, że C++ jest językiem skomplikowanym to dołączenie do niego zawiłości CLR z niedoskonałą (w porównaniu do C#, gdzie twórcy języka mogli zaprojektować go od zupełnych podstaw) składnią prowadzi do katastrofy. A idzie nam jeszcze Windows 8, WinRT i kolejna odmiana C++, czyli C++/CX z podobnymi, ale znów innymi i prowadzącymi do kolejnych nieporozumień rozszerzeniami.

Spójrzcie z resztą na tą bardzo prostą klasę, którą napisałem jakiś czas temu w C#.

namespace Rev.PhotoAlbum.Application.Modules
{
    using Autofac;

    using NHibernate;
    using NHibernate.Cfg;

    using Rev.PhotoAlbum.Model.Configuration;
    using Rev.PhotoAlbum.Model.Repositories;

    public class DataAccessModule : Module
    {
        protected override void Load(ContainerBuilder builder)
        {
            var configuration = Configurator.BuildConfiguration();
            var sessionFactory = Configurator.BuildSessionFactory(configuration);

            RegisterComponents(builder, configuration, sessionFactory);
        }

        private void RegisterComponents(ContainerBuilder builder, Configuration configuration, ISessionFactory sessionFactory)
        {
            builder.RegisterInstance(configuration).As<Configuration>().SingleInstance();
            builder.RegisterInstance(sessionFactory).As<ISessionFactory>().SingleInstance();
            builder.Register(x => x.Resolve<ISessionFactory>().OpenSession()).As<ISession>().InstancePerLifetimeScope();

            builder.RegisterAssemblyTypes(typeof(IRepository<>).Assembly)
                .Where(t => t.IsClosedTypeOf(typeof(Repository<,>)))
                .AsClosedTypesOf(typeof(IRepository<>));
        }
    }
}

Nie ma co prawda w kodzie komentarzy, ale dzięki odpowiedniej konstrukcji frameworków, z których korzystam kod ten można.. po prostu przeczytać - ciągiem. Układają się z tego zdania i na pierwszy rzut oka można się domyślić co się dzieje - chodzi mi zwłaszcza o metodę RegisterComponents. Jeżeli ktoś nigdy z AutoFaca nie korzystał, a ma minimalną wiedzę z teorii na temat kontenerów IoC mógłby własny kod napisać po jedno, dwu-krotnym przeczytaniu mojego.

A teraz ten sam kod w C++/CLI.
Najpierw oczywiście plik nagłówkowy, jako sumienni programiści C++ nie wrzucamy wszystkiego do jednego pliku.

#pragma once

namespace Rev
{
	namespace PhotoAlbum
	{
		namespace Application
		{
			namespace Modules
			{
				ref class DataAccessModule : public Autofac::Module
				{
				private:
					void RegisterComponents(Autofac::ContainerBuilder^ builder, NHibernate::Cfg::Configuration^ configuration, NHibernate::ISessionFactory^ sessionFactory);
				protected:
					virtual void Load(Autofac::ContainerBuilder^ builder) override;
				};
			}
		}
	}
}

Ah, no tak. C++ nie obsługuje zagnieżdżonych przestrzeń nazw w jednej instrukcji, musimy rozbić je na kilka. Tutaj pedantycznie trzymam się formatowania, które sobie przyjąłem, możemy zawsze wrzucić wszystkie namespace'y do jednej linijki albo - jak polecają w internecie - stworzyć sobie makro. A jak.
Rozciągnął się kod niesamowicie, no, ale przecież mamy na uwadze zasadę nieużywania using namespace w plikach nagłówkowych, żeby nie potworzyć żadnych konfliktów nazw w plikach, gdzie będziemy nasz plik nagłówkowy dołączać.

using namespace Rev::PhotoAlbum::Application::Modules;

using namespace Autofac;

using namespace NHibernate;
using namespace NHibernate::Cfg;

using namespace Rev::PhotoAlbum::Model::Configuration;
using namespace Rev::PhotoAlbum::Model::Repositories;

void DataAccessModule::Load(ContainerBuilder^ builder)
{
	auto configuration = Configurator::BuildConfiguration();
	auto sessionFactory = Configurator::BuildSessionFactory(configuration);

	RegisterComponents(builder, configuration, sessionFactory);
}

ISession^ ISessionResolve(IComponentContext^ context)
{
	return ResolutionExtensions::Resolve<ISessionFactory^>(context)->OpenSession();
}

bool IRepositoryWhereCondition(System::Type^ type)
{
	return TypeExtensions::IsClosedTypeOf(type, Repository::typeid);
}

void DataAccessModule::RegisterComponents(ContainerBuilder^ builder, Configuration^ configuration, ISessionFactory^ sessionFactory)
{
	RegistrationExtensions::RegisterInstance(builder, configuration)->As<Configuration^>()->SingleInstance();
	RegistrationExtensions::RegisterInstance(builder, sessionFactory)->As<ISessionFactory^>()->SingleInstance();

	RegistrationExtensions::Register<ISession^>(builder, gcnew System::Func<IComponentContext^, ISession^>(&ISessionResolve))->As<ISession^>()->InstancePerLifetimeScope();

	RegistrationExtensions::AsClosedTypesOf(RegistrationExtensions::Where(RegistrationExtensions::RegisterAssemblyTypes(builder, IRepository::typeid->Assembly),
		gcnew System::Func<System::Type^, bool>(&IRepositoryWhereCondition)), IRepository::typeid);
}

Chciałbym zwłaszcza zwrócić na końcówkę metody RegisterComponents. Konia z rzędem temu, kto rozszyfruje co się tutaj dzieje. Niestety (albo stety) bardzo wiele frameworków, które ułatwiają (taa) pisanie w .NET korzystają z metod rozszerzających (z resztą zdaje się, że bardzo dobrze, zgodnie z zasadą open/closed). Z jakiś powodów najzwyczajniej nie są one składniowo obsługiwane w C++/CLI i trzeba wywoływać je ręcznie, z klasy, w której są zdefiniowane i przekazywać ręcznie obiekt. Bez tracenia dodatkowego czasu na znalezienie chociażby nazwy tej klasy się nie obejdzie. Kolejna rzecz to lambdy, które w C# niesamowicie ułatwiają sprawę, a w C++/CLI powodują, że musimy je wydzielić do zewnętrznej metody. Lambdy z C++11 nawet w Visual Studio 11 nie są z nimi kompatybilne. No trudno.
Ktoś mógłby powiedzieć, że klepanie nawet takiego kodu z IntelliSense nie będzie trudne. Nawet dodali do niego obsługę C++/CLI. Bardziej przykra sprawa jest taka, że na co trzeciej bardziej rozbudowanej linijce z typami generycznymi po prostu się wykłada i zaczyna krzyczeć o błędach. Których de facto nie ma, bo kod się kompiluje i działa.

Wykorzystanie w C#:

var builder = new ContainerBuilder();
builder.RegisterModule<Modules.DataAccessModule>();
var container = builder.Build();
var repository = container.Resolve<IRepository<Person>>();

I C++/CLI:

auto builder = gcnew ContainerBuilder();
RegistrationExtensions::RegisterModule<Modules::DataAccessModule^>(builder);
auto container = builder->Build(Builder::ContainerBuildOptions::Default);
auto repository = ResolutionExtensions::Resolve<IRepository<Person^>^>(container);

Nie domyślił się wartości domyślnej parametru, a IntelliSense rzucił malowniczy błąd w ostatniej linii o tym, że wywołanie pasuje do wielu przeładowań oraz jednocześnie ma za mało parametrów.

Nie mówię, że pisanie w C++/CLI nie ma w ogóle sensu. Ma, ale są to naprawdę marginalne zastosowania, gdy nie wystarcza nam P/Invoke i interop w C#. Ja jeszcze nigdy nie musiałem go naprawdę użyć. Wy pewnie też nie.

1

co to znaczy "portowanie bibliotek"?

1

Pewnie chodziło o wrappery.

A tak w temacie, to mam też wrażenie, że początkujący często nieświadomie piszą w C++/CLI. W końcu nazwa podobna:).

0

Co się dziwić, znaczna część programistów (edit: tzn tych bardzo początkujących) C# myśli, że (C#) to jest jakiś następca C++.

1

@Rev - myślę że problem z twoją argumentacją wynika z tego że próbujesz patrzeć na C++/CLI jak na C# z inną składnią. Chociażby AutoFac którego przykład podałeś - był on projektowany specjalnie pod C#. To że w ogóle działa pod C++/CLI to wynik zgodności języków zarządzanych a nie jakieś celowe zagranie.

Równie kiepskie podejście do C++/CLI to patrzenie na niego jako na C++ z dodatkową biblioteką standardową. Momentalnie zaczynają pojawiać się problemy z łączeniem zarządzanych i niezarządzanych fragmentów.

4
Wibowit napisał(a)

Co się dziwić, znaczna część programistów C# myśli, że (C#) to jest jakiś następca C++.

Tja, a programiści JavaScript myślą, że to jakiś następca Javy.
Jeśli ktoś tak uważa, to programistą C# nie jest i raczej nie będzie.

Tezcatlipoca napisał(a)

@Rev - myślę że problem z twoją argumentacją wynika z tego że próbujesz patrzeć na C++/CLI jak na C# z inną składnią.

Nie zauważyłem, żeby @Rev miał problem z argumentacją, to chyba Ty masz jakiś problem z jego argumentami. To nie on patrzy na C++/CLI w ten sposób, lecz ci, którzy próbują go tak używać i na siłę robić z niego język ogólnego przeznaczenia.

Jakieś dziecko przeczyta w internecie, że Prawdziwi Profesjonalni Programiści piszą w C++, ściąga VC++, tworzy projekt i zaczyna się magia, dzięki której w 5 minut robi GUI jak w prawdziwym programie, więc myśli, że dalej będzie już z górki. Dzieciak nie wie, że to nie jest C++, dzieciak nie wie o co chodzi z tym CLI, nie wie, co to .NET - nie wie nic, ale myśli, że uczy się Jedynego Słusznego Języka Do Pisania Gier. I stąd wynikają wspomniane przez Ciebie problemy z łączeniem zarządzanych i niezarządzanych fragmentów kodu.

2
somekind napisał(a)
Tezcatlipoca napisał(a)

@Rev - myślę że problem z twoją argumentacją wynika z tego że próbujesz patrzeć na C++/CLI jak na C# z inną składnią.

Nie zauważyłem, żeby @Rev miał problem z argumentacją, to chyba Ty masz jakiś problem z jego argumentami. To nie on patrzy na C++/CLI w ten sposób, lecz ci, którzy próbują go tak używać i na siłę robić z niego język ogólnego przeznaczenia.

Ja tylko zauważam że porównanie C++/CLI i C# na podstawie przepisywania kodu z tego drugiego do pierwszego jest trochę nieodpowiednie.
Te języki mają po prostu zupełnie inne zastosowania i to jest chyba dla wszystkich oczywiste?

2

Te języki mają po prostu zupełnie inne zastosowania i to jest chyba dla wszystkich oczywiste?

W całym moim wywodzie chodzi właśnie o to, żeby to pokazać. Gdybyś czytał wszystkie wątki w dziale .NET i C++ to wiedziałbyś, że jest to sprawa wielce nieoczywista dla niezliczonej ilości osób.
A ten nieudany kod w C++/CLI, który zaprezentowałem tylko to potwierdza. Ten przykład miał pokazać, że pisanie kodu aplikacji biznesowych w środowisku .NET w C++/CLI jest bez sensu, gdy mamy C#.

0

Zawsze mi się wydawało, że głównym celem C++/CLI jest ułatwienie przeniesienia sporych aplikacji na platformę .NET bez potrzeby przepisywania całego kodu do C#, tylko pisząc w nim nowe elementy.

1
Tezcatlipoca napisał(a)

Ja tylko zauważam że porównanie C++/CLI i C# na podstawie przepisywania kodu z tego drugiego do pierwszego jest trochę nieodpowiednie.

A na jakiej innej zasadzie można porównać przydatność języków do danego zastosowania, jeśli nie poprzez zwięzłość i czytelność kodu?

0
somekind napisał(a)
Wibowit napisał(a)

Co się dziwić, znaczna część programistów C# myśli, że (C#) to jest jakiś następca C++.

Tja, a programiści JavaScript myślą, że to jakiś następca Javy.
Jeśli ktoś tak uważa, to programistą C# nie jest i raczej nie będzie.

Przecież na tym forum niejeden początkujący programista C# tak myśli...

A na jakiej innej zasadzie można porównać przydatność języków do danego zastosowania, jeśli nie poprzez zwięzłość i czytelność kodu?

Haskell jest zwięzły, czytelny, a obecne wersje kompilatorów tworzą dość wydajny kod.

Tak mogą myśleć jacyś newbie i totalni laicy, a nie ktoś, kto ma jako takie pojęcie o tych językach. :)

No napisałem początkujący (czyli newbie). Zbieżność nazw C++ i C# jest dla nich argumentem za tym, żeby po C++ wybrać C#, a nie np Javę, Scalę, Ruby, Pythona, etc Podobnie w drugą stronę, był tu przypadek typa, który myślał, że C++ to taki inny C# i wrzucał public i private przed każdą deklaracją.

stfu:
Zgoda, zapomniałem dopisać początkujących.

2

Podany przykład jest wybitnie tendencyjny, bo opiera się na tym, że C++/CLI nie ma extension methods. Gdyby Microsoft w końcu zaimplementował rozszerzenia pod C++/CLI, ten argument by znikł.

Można również pokazać argument odwrotny, jakiegoś kodu używającego biblioteki napisanej w C++, najlepiej obiektowo z szablonami, i jej użycie spod C++/CLI i spod C#.

Ubolewam jednak, że M$ od kilku wersji kompletnie olewa C++/CLI tłumacząc się „brakiem zasobów”, w rezultacie czego język stanął na poziomie .Net 2.0. Z tego powodu użycie konstrukcji specyficznych dla nowszych wersji frameworka jest skomplikowane albo niemożliwe.
To nie jest wina języka, ale mizernego jego rozwoju.

3

Pytanie do czego przydaje się w ogóle C++/CLI? Bo poza celowym łączeniem kodu zarządzanego i niezarządzanego jakoś zastosowań nie widzę. Sam bym myślał o użyciu C++/CLI tylko do:

  • napisania biblioteki dll, która będzie wrapperem importującym wiele funkcji natywnych lub klas z natywnej bibloteki .dll. Potem ten wrapper używany w projekcie w C# jako normalne assembly. O ile mi wiadomo, jeśli niezarządzana biblioteka dll eksportuje bezpośrednio klasy to użycie jej w C# bezpośrednio może okazać się niemożliwe
  • uzyskania sytuacji odwrotnej: mam projekt w natywnym C++, np mfc albo C++ Builder i zachciało mi się użyć funkcjonalności z .net. Robię wrappera, który eksportuje niezarządzane funkcje możliwe do użycia w projekcie w C++ niezarządzanym.

Ale po jaką cholerę pisać całe aplikacje w C++/CLI - tego nie rozumiem. Zresztą, pomijając to wszystko co zostało już powiedziane, jedyne co osiągniemy to aplikacja w WinForms z pewnymi ograniczeniami, nie ma wpf, nie ma webserwisów itd itp.

1

napisania biblioteki dll, która będzie wrapperem importującym wiele funkcji natywnych lub klas z natywnej bibloteki .dll
Biblioteka może być wielka, specjalistyczna, używać konstrukcji typowych dla C++. Samo napisanie wrappera może być zbyt dużym zadaniem, większym niż zaciśnięcie zębów i napisanie GUI w C++/CLI.

C++/CLI jest dla mnie najbardziej naturalnym wyborem jeśli mam na szybko napisać aplikację GUI, pod Windows, w C++.
Prawie w C++, powiedzmy.

Jeśli jednak nie ma argumentów przemawiających za C++, wybieram C# naturalnie.

0

Biblioteka może być wielka, specjalistyczna, używać konstrukcji typowych dla C++

Może. Ale praktyka pokazuje, że często jest tak że potrzebujemy użyć tylko niewielkiej części dostępnych funkcjonalności, co łatwo ubrać w metody które da się potem wykorzystać w C#. Mowa oczywiście nie o setkach plików .cpp tylko np. zamkniętej bibliotece dll napisanej w c++, która eksportuje różne rzeczy.

Zreszta, zaciskanie zębów nic nie pomoże jeśli np. chcemy wpf zamiast przestarzałego winformsa.

2
Azarien napisał(a)

C++/CLI jest dla mnie najbardziej naturalnym wyborem jeśli mam na szybko napisać aplikację GUI, pod Windows, w C++.

Ale po co pisać aplikację GUI pod Windows w udziwnionym C++? Jakiś praktyczny przykład?

0

Pytacie czemu ludzie zaczynają od C++/CLI zamiast C#? Ano przez takie wpisy np: http://www.dobreprogramy.pl/Protected_48/CCLI-Replikacja-prostego-kalkulatora,31485.html - pytanie PO CO było to robić w C++ zamiast w C#?

1

No,właśnie C# jest następcą C++,bo C# zostało stworzone w 2000 a C++ w 1979
Dane wzięte żywcem z wikipedii :)

0

Zastanawiające, że ostatnio mamy wysyp pytań na forum dotyczących Windows Forms w C++/CLI.

I to w VS2013, gdzie nie da się przypadkiem kliknąć takiego typu projektu, bo go nie ma - trzeba wiedzieć że coś takiego istnieje i albo otworzyć projekt utworzony pod VS2010, albo stworzyć projekt konsolowy i pozmieniać w opcjach.

1 użytkowników online, w tym zalogowanych: 0, gości: 1