- Start constexpr N threads at runtime - 5 Updates
- Design patterns with T*, where T is never defined - 9 Updates
- Code duplication with template type - 4 Updates
- what do you think about this? - 3 Updates
- Structured binding considered harmful - 2 Updates
- My thoughts and prayers - 2 Updates
Frederick Gotham <cauldwell.thomas@gmail.com>: Mar 23 01:45AM -0700 If the amount of threads I want to create is a constexpr, but if this constexpr can be changed between compilations (for example from 3 to 4 depending on the platform), and if I don't want to allocate the "std::thread" objects on the heap, then is there a better way to do it than the following? auto main(void) -> int { array<aligned_union<0,thread>::type, g_N_threads> storage_for_threads; for ( unsigned i = 0; i != g_N_threads; ++i ) { ::new(&storage_for_threads[i]) thread(EntryPoint, i); } /* Do stuff */ for ( unsigned i = 0; i != g_N_threads; ++i ) { thread &t = *reinterpret_cast<thread*>(&storage_for_threads[i]); t.join(); t.~thread(); } } Previously when the amount of threads was set at 3, I just did: thread t0(EntryPoint, 0); thread t1(EntryPoint, 1); thread t2(EntryPoint, 2); but now I want something that automagically adjusts itself whenever I change the constexpr g_N_threads. |
Melzzzzz <Melzzzzz@zzzzz.com>: Mar 23 10:13AM > for ( unsigned i = 0; i != g_N_threads; ++i ) > { > ::new(&storage_for_threads[i]) thread(EntryPoint, i); Why do you think this is better then std::vector<thread>? Anyway only char[] is safe for placement new... -- press any key to continue or any other to quit... U ničemu ja ne uživam kao u svom statusu INVALIDA -- Zli Zec Svi smo svedoci - oko 3 godine intenzivne propagande je dovoljno da jedan narod poludi -- Zli Zec Na divljem zapadu i nije bilo tako puno nasilja, upravo zato jer su svi bili naoruzani. -- Mladen Gogala |
Paavo Helde <myfirstname@osa.pri.ee>: Mar 23 12:33PM +0200 On 23.03.2020 10:45, Frederick Gotham wrote: > { > ::new(&storage_for_threads[i]) thread(EntryPoint, i); > } You said that you don't want to allocate thread objects on heap, but why? Is this some obsession about saving a nanosecond at the start of the program? Is your program meant for high-frequency trading at NYSE? No? Then what's wrong with a std::vector<std::thread>? I would understand this if you said this is about learning placement new and implementing one's own containers from scratch. However, for holding thread objects this does not make any sense as thread creation is anyway a massively expensive operation when compared to a dynamic memory allocation. > for ( unsigned i = 0; i != g_N_threads; ++i ) > { > thread &t = *reinterpret_cast<thread*>(&storage_for_threads[i]); You should avoid reinterpret_cast if possible, it's not well-behaved in general. In this example you should either remember the addresses returned by placement new, or use std::launder() for making the result pointer legal (std::launder is meant for low-level container implementers, if you insist on doing that you need to learn about it). |
"Alf P. Steinbach" <alf.p.steinbach+usenet@gmail.com>: Mar 23 12:27PM +0100 On 23.03.2020 09:45, Frederick Gotham wrote: > thread t1(EntryPoint, 1); > thread t2(EntryPoint, 2); > but now I want something that automagically adjusts itself whenever I change the constexpr g_N_threads. The above code is just weird. You can do things like this: #include <stdlib.h> #include <array> #include <chrono> #include <iostream> #include <mutex> #include <thread> using std::array, std::mutex, std::thread, std::cout, std::endl; namespace this_thread = std::this_thread; namespace chrono = std::chrono; void output( const int i ) { static mutex mx_streams; mx_streams.lock(); cout << i << endl; mx_streams.unlock(); } void thread_func( const int i ) { this_thread::sleep_for( chrono::milliseconds( rand() % 10 ) ); output( i ); } auto main() -> int { array< thread, 7 > threads; // Start 'em up. srand( 42 ); for( int i = 0, n = int( threads.size() ); i < n; ++i ) { threads[i] = thread( thread_func, i ); } // Join all. for( int i = 0, n = int( threads.size() ); i < n; ++i ) { threads[i].join(); } } - Alf |
"Öö Tiib" <ootiib@hot.ee>: Mar 23 05:03AM -0700 On Monday, 23 March 2020 10:45:56 UTC+2, Frederick Gotham wrote: > auto main(void) -> int > { > array<aligned_union<0,thread>::type, g_N_threads> storage_for_threads; Use simply: array<thread, g_N_threads> threads{}; Threads will be default constructed in "not representing a thread" state. > for ( unsigned i = 0; i != g_N_threads; ++i ) > { > ::new(&storage_for_threads[i]) thread(EntryPoint, i); That becomes: threads[i] = thread(EntryPoint, i); > thread &t = *reinterpret_cast<thread*>(&storage_for_threads[i]); > t.join(); > t.~thread(); And that is just: if (threads[i].joinable()) threads[i].join(); > thread t1(EntryPoint, 1); > thread t2(EntryPoint, 2); > but now I want something that automagically adjusts itself whenever I change the constexpr g_N_threads. Yes but I did not get the point of usage of aligned_union. The std::thread is itself most likely just operating system resource handle enwrapped, perhaps with size of single pointer. |
"Alf P. Steinbach" <alf.p.steinbach+usenet@gmail.com>: Mar 22 11:37PM +0100 On 22.03.2020 23:14, Maciej Sobczak wrote: > for example T being a "base class" for A, and B operating on that > "base"), but my question is whether you have seen a wider use of this > pattern. Not /exactly/ the same, but the handle types in the Win32 API match. > The type safety is a defining added value of this pattern and it > might have a wider applicability. Can you share your experiences? A main problem in the Win32 API is when there are several conceptual handle types that during their lifetimes, between creation and destruction, act as the same kind, like a pointer to base class. In particular HDC (handle to device context), if I recall correctly. Depending on how it was created such handle must be destroyed by a corresponding function, but the type does not encode that. - Alf |
Melzzzzz <Melzzzzz@zzzzz.com>: Mar 23 05:16AM > Without this idiom, the pointer would be passed around in the form of > void*, but the use of class T adds some type safety, enables > overloading, etc. What type safety? It is worse as you don't need reinterpret cast in case of passing void*.... -- press any key to continue or any other to quit... U ničemu ja ne uživam kao u svom statusu INVALIDA -- Zli Zec Svi smo svedoci - oko 3 godine intenzivne propagande je dovoljno da jedan narod poludi -- Zli Zec Na divljem zapadu i nije bilo tako puno nasilja, upravo zato jer su svi bili naoruzani. -- Mladen Gogala |
Melzzzzz <Melzzzzz@zzzzz.com>: Mar 23 05:19AM > particular HDC (handle to device context), if I recall correctly. > Depending on how it was created such handle must be destroyed by a > corresponding function, but the type does not encode that. Problem with win32 API is because it uses DWORD integer and recently, DWORD_PTR so you must use reinterpret_cast :( -- press any key to continue or any other to quit... U ničemu ja ne uživam kao u svom statusu INVALIDA -- Zli Zec Svi smo svedoci - oko 3 godine intenzivne propagande je dovoljno da jedan narod poludi -- Zli Zec Na divljem zapadu i nije bilo tako puno nasilja, upravo zato jer su svi bili naoruzani. -- Mladen Gogala |
Paavo Helde <myfirstname@osa.pri.ee>: Mar 23 07:41AM +0200 On 23.03.2020 7:16, Melzzzzz wrote: >> overloading, etc. > What type safety? It is worse as you don't need reinterpret cast > in case of passing void*.... Double static_cast to void* and back to T* is equivalent to a reinterpret_cast. Actually, with the "T*" interfaces, one does not need any reinterpret_casts in the C++ implementation code which sees the class definitions. Upcasting in a class hierarchy is implicit and downcasting can be done by a single static_cast. reinterpret_cast would be needed in the client code, which does not see class definitions. However, reinterpret_cast is very under-specified and may easily fail (especially in case of multiple or virtual inheritance, but I'm pretty sure the results are formally undefined or implementation-specific also in simpler scenarios). So I would ban such casts in the client side code and provide explicit conversion functions instead if the client code really needs to cast between different derived class pointer types. In short, reinterpret_cast is not a proper way to implement anything, I'm a bit surprised if Lakos really recommends it. |
Melzzzzz <Melzzzzz@zzzzz.com>: Mar 23 05:43AM >> in case of passing void*.... > Double static_cast to void* and back to T* is equivalent to a > reinterpret_cast. Yes, but what's the point of void* then? We used char* before void, and now when void* is introduced, we should use it. > between different derived class pointer types. In short, > reinterpret_cast is not a proper way to implement anything, I'm a bit > surprised if Lakos really recommends it. +1 -- press any key to continue or any other to quit... U ničemu ja ne uživam kao u svom statusu INVALIDA -- Zli Zec Svi smo svedoci - oko 3 godine intenzivne propagande je dovoljno da jedan narod poludi -- Zli Zec Na divljem zapadu i nije bilo tako puno nasilja, upravo zato jer su svi bili naoruzani. -- Mladen Gogala |
Paavo Helde <myfirstname@osa.pri.ee>: Mar 23 07:58AM +0200 On 23.03.2020 0:14, Maciej Sobczak wrote: > Without this idiom, the pointer would be passed around in the form of void*, but the use of class T adds some type safety, enables overloading, etc. > Lakos described this pattern as a way to hide inheritance between types that are supposed to be hidden behind a wrapper facade (like for example T being a "base class" for A, and B operating on that "base"), but my question is whether you have seen a wider use of this pattern. > The type safety is a defining added value of this pattern and it might have a wider applicability. Can you share your experiences? This idiom is often used in library interfaces, mostly in C, but also in C++. For example, the main "handle" type SSL in openssl 1.1 is defined as: typedef struct ssl_st SSL; without definition of ssl_st. The public functions are defined in terms of SSL*. This is in contrast to openssl 1.0 where the struct definitions were public; the change was specifically done to hide implementation details - see "https://wiki.openssl.org/index.php/OpenSSL_1.1.0_Changes". Another example are Python C bindings (CPython), where most of the interfaces are defined in terms of "base class" pointer PyObject*, but in reality there is no PyObject object, all objects are of some "derived" class like PyLongObject, etc. So the implementation code contains a lot of casting which are effectively reinterpret_cast-s because there is no inheritance in C and all those structs are technically unrelated. In C++ the dreadful reinterpret_cast is actually not needed if things are done properly, static_cast is guaranteed to do the correct thing in derived class hierarchies. Of course, using such raw pointers more suits a C-style interface. BTW, it is a great idea to provide a C-style interface for a C++ library, this allows for its much wider use and avoids some technical complications related to the lack of standardized ABI in C++. However, in pure C++ one typically expects to have automatic resource release in some destructor where needed, so the raw pointer should be packed into some small "handle" class, either std::shared_ptr or something more elaborate. In this way we arrive to the pimpl idiom, which is indeed often used in C++ for hiding implementation details. |
Melzzzzz <Melzzzzz@zzzzz.com>: Mar 23 06:12AM > in C++. For example, the main "handle" type SSL in openssl 1.1 is > defined as: > typedef struct ssl_st SSL; Actually in Haskell this idiom is necessary as then one introduce, type check in compiler. eg: newtype CB3 = CB3 (Ptr CB3) type Callback3 = (PtrCallback -> PtrCallback1 -> PtrCallback -> PtrCallback1 -> PtrCallback -> PtrCallback2 -> CB3 -> PtrCallback4 -> IO ()) type PtrCallback3 = FunPtr Callback3 No casts needed just to associate type to opaque data. -- press any key to continue or any other to quit... U ničemu ja ne uživam kao u svom statusu INVALIDA -- Zli Zec Svi smo svedoci - oko 3 godine intenzivne propagande je dovoljno da jedan narod poludi -- Zli Zec Na divljem zapadu i nije bilo tako puno nasilja, upravo zato jer su svi bili naoruzani. -- Mladen Gogala |
Maciej Sobczak <see.my.homepage@gmail.com>: Mar 23 04:13AM -0700 > >> class B { public: void foo(T * p); // reinterpret_cast<something *>(p) > >> }; > > What type safety? Without client-side casts, you can only call B::foo with what you got from A::getT. If this code pattern was based on void*, then you might be able to call B::foo with whatever and since conversion to void* is implicit, there would be no hint that there is anything wrong with the code. In particular, no help from the compiler. So, using T* binds the producer and consumer in a way that only a proper use compiles without errors: my_b.foo(my_a.getT()); // OK my_b.foo(my_c.getU()); // error > reinterpret_casts in the C++ implementation code which sees the class > definitions. Upcasting in a class hierarchy is implicit and downcasting > can be done by a single static_cast. But there is no hierarchy here and the value of this idiom is that there is no need for any hierarchy. > reinterpret_cast would be needed in the client code No, see above. Actually, it is the presence of reinterpret_cast that might indicate a potential problem, or at least a usage that deserves attention in code reviews. > So I would ban such casts in the client side code Again: there are no casts in the client side code. That's the whole point. The cast is an implementation detail of the classes/functions that participate in this idiom. > I'm a bit > surprised if Lakos really recommends it. He does not *recommend* it in the sense of endorsement, but presents it as an idiom that has emerged and is in use. The other responses about Win and Python APIs show that this idiom is not even a recent one and can be found in pure C, too. -- Maciej Sobczak * http://www.inspirel.com |
Paavo Helde <myfirstname@osa.pri.ee>: Mar 23 01:48PM +0200 On 23.03.2020 13:13, Maciej Sobczak wrote: >> definitions. Upcasting in a class hierarchy is implicit and downcasting >> can be done by a single static_cast. > But there is no hierarchy here and the value of this idiom is that there is no need for any hierarchy. Ah, I see I have misunderstood the concept and spoke about something else. Somehow I assumed that A and B are themselves hidden implementation classes, and I assumed T is finally defined somewhere. Now it seems there is actually some unrelated hidden class C, and a C* pointer is reinterpret_cast into T*, and back to C* later. But what's the point? Why shouldn't C be just forward-declared in the header with 'class C'? What's the benefit of having a non-existent class T? As Alf said, Win32 interfaces are a bit similar. For example, FILE is defined as typedef struct _iobuf { void* _Placeholder; } FILE; It looks like this is not the actual implementation, meaning that any FILE* pointer is first cast to something else in the implementation code. I have always assumed this is just another legacy madness from Microsoft which was maybe necessary in 16-bit DOS, or something. I do not recall such usage in genuine C++ code. |
Frederick Gotham <cauldwell.thomas@gmail.com>: Mar 23 01:55AM -0700 The only difference between the following two functions is that I have ::Encryption instead of ::Decryption Is there any way of getting rid of this code duplication? (By the way I realise I can use the C preprocessor but I'd like to find a better solution before resorting to that). CryptoPP::BlockTransformation &GetAlgorithm_Forward(unsigned const i) { static thread_local CryptoPP:: AES ::Encryption a(Kalpha,g_KEYSIZE); static thread_local CryptoPP::Twofish::Encryption b(Kalpha,g_KEYSIZE); static thread_local TDES16::Encryption c(Kalpha,g_KEYSIZE); static thread_local CryptoPP::CAST256::Encryption d(Kalpha,g_KEYSIZE); static thread_local CryptoPP::Serpent::Encryption e(Kalpha,g_KEYSIZE); switch ( i ) { case 0: return a; case 1: return b; case 2: return c; case 3: return d; case 4: return e; } assert(0 == "Control should never reach here"); } CryptoPP::BlockTransformation &GetAlgorithm_Backward(unsigned const i) { static thread_local CryptoPP:: AES ::Decryption a(Kalpha,g_KEYSIZE); static thread_local CryptoPP::Twofish::Decryption b(Kalpha,g_KEYSIZE); static thread_local TDES16::Decryption c(Kalpha,g_KEYSIZE); static thread_local CryptoPP::CAST256::Decryption d(Kalpha,g_KEYSIZE); static thread_local CryptoPP::Serpent::Decryption e(Kalpha,g_KEYSIZE); switch ( i ) { case 0: return a; case 1: return b; case 2: return c; case 3: return d; case 4: return e; } assert(0 == "Control should never reach here"); } |
Maciej Sobczak <see.my.homepage@gmail.com>: Mar 23 04:29AM -0700 > ::Encryption > instead of > ::Decryption And from the remaining code I can deduce that these are classes deriving from a common base class BlockTransformation, with very likely a single operation that does one or the other thing. Note, however, that the similarity of class names is just a coincidence and that was your choice. If you have pairs of classes that are so tightly bound that it does not make sense to use them separately (or to mix them between pairs), then perhaps having them as separate classes was a mistake in the first place? Why these classes are separate if in fact they are bound by an algorithm? What about having a single class with two operations instead? Like: class AES : public ... { public: virtual ... encrypt(...); virtual ... decrypt(...); // ... }; Then there would be duplication in your code, and the number of leaf classes would be halved. A single class would then encapsulate the single algorithm (and there is a chance that both encrypt and decrypt operations reuse a common computational mechanics anyway). In other words, you have code duplication, because you have designed your class hierarchy with too fine granularity in the first place. I don't claim that the above is certainly better, without knowing the rest of your design - and there is a chance that removing some code duplication in one place will generate it in some other place. You have to judge this trade off on your own. :-) -- Maciej Sobczak * http://www.inspirel.com |
"Öö Tiib" <ootiib@hot.ee>: Mar 23 04:34AM -0700 On Monday, 23 March 2020 10:55:54 UTC+2, Frederick Gotham wrote: > } > assert(0 == "Control should never reach here"); > } I would do it using a random idea of selecting between types compile time. Like that: #include <iostream> // I/O of current garbage #include <type_traits> // std::conditional #include <cassert> // old school assert // just classes for demoing enum Direction {Error, Decrypt, Encrypt}; struct Encryption {Encryption() {std::cout << "Encryption made\n";} }; struct Decryption { Decryption() {std::cout << "Decryption made\n";} }; struct Disaster { Disaster() {assert(not "Disaster useful");} }; template<Direction D> using Transformation = std::conditional<D == Decrypt, Decryption , typename std::conditional<D == Encrypt, Encryption , Disaster >::type>::type; int main() { Transformation<Encrypt> a; Transformation<Decrypt> b; Transformation<Error> c; } Trying on gcc version 9.2.0 gave that (did not try elsewhere): a.out: main.cpp:9: Disaster::Disaster(): Assertion `not "Disaster useful"' failed. Encryption constructed Decryption constructed bash: line 7: 15254 Aborted (core dumped) ./a.out |
"Alf P. Steinbach" <alf.p.steinbach+usenet@gmail.com>: Mar 23 12:43PM +0100 On 23.03.2020 09:55, Frederick Gotham wrote: > } > assert(0 == "Control should never reach here"); > } In this code CryptoCPP is a namespace and AES, Twofish, TDES16, CAST256 and Serpent are classes. You can do things like (off the cuff) struct Which_way{ enum Enum{ encrypt, decrypt }; }; template< class Cipher, Which_way::Enum > struct Transformation_; template< class Cipher > struct Transformation_<Cipher, Which_way::encrypt> { using T = Cipher::Encryption; }; template< class Cipher > struct Transformation_<Cipher, Which_way::decrypt> { using T = Cipher::Decryption; }; template< Which_way::Enum direction > auto algorithm( const int i ) -> CryptoPP::BlockTransformation& { static thread_local typename Transformation_<CryptoPP::AES, direction>::T a(Kalpha,g_KEYSIZE); // Etc. } - Alf |
alelvb <alelvb@inwind.it>: Mar 23 10:24AM +0100 Il 22/03/20 21:36, Jorgen Grahn ha scritto: > I can't summarize it ... one problem was giving associativity and > precendence for such operators. > /Jorgen I see, but it sounded to me as a great boos to the language. Mr Stroustrup knows his child better than anyone else, so if He decided not to give it this feature there must have been serious problems concerned with it. Thank you for your reply. alessandro |
alelvb <alelvb@inwind.it>: Mar 23 10:29AM +0100 Il 22/03/20 22:27, Sam ha scritto: > Foo a = b ^ c; > For the dot and cross product you can overload + and *, and so on. > That's as much as you can do, within the existing boundaries of C++. In this specific case you are right, but in other cases there is not a correlation between standard C++ operators and the symbols required. thank you for your reply, alessandro |
alelvb <alelvb@inwind.it>: Mar 23 11:48AM +0100 Il 22/03/20 23:00, Paavo Helde ha scritto: > this thing could be standardized (the 25-30 years so far have shown > mixed results). Too bad all the code will be generated by AI bots by > that time... I didn't imagine that this expression could be used with the hypothetical user defined symbol. I'm just a lover of computer programming, self made, but with few technical knowledge; but I like to spend my spare time playing with a compiler and a text editor (or with an IDE that helps with the debuging). thank you for your reply, alessandro |
danielaparker@gmail.com: Mar 22 05:11PM -0700 On Sunday, March 22, 2020 at 7:06:23 PM UTC-4, Sam wrote: > Yes. And I have somewhat of a difficult time comprehending why anyone would > get offended at anything someone they don't even know If I could be permitted one last post. The language that you and some others use - "wanna-be", "limited capacity", "Here are your toys. You can go home now" - is the language of abuse. It is the language that abusers use, to their children, to their partners, usually to people that they identify as weak, to the bitch that "had it coming", whatever. The heavyweights that were once here - Stroustrup, James Kanze, all of them - never used such language, ever. No moderated technical group ever permits such language because of how destructive to communities it is. > I don't get it I know. Daniel |
Sam <sam@email-scan.com>: Mar 22 10:16PM -0400 > the language that abusers use, to their children, to their partners, usually > to people that they identify as weak, to the bitch that "had it coming", > whatever. The notion that mere spoken or written words are comparable to actual physical abuse to children or spouses is laughable and insulting in of itself. Not to me, but insulting to anyone who actually has to deal with real, human abuse. Go visit a hospital emergency room, and see the faces and bodies of those children or spouses, that look like processed meat when they wheeled in, if you want to see what real abuse looks like. Or a local justice of the peace, when they're working on civil or criminal restraint orders. Now that's real abuse for you. Your attempt to wrap yourself around in a flag of abuse is laughable, and pathetic. Trying to make any kind of a meaningful comparison between some shithead getting called a shithead to their virtual cyber-face, and actual one-on-one physical or even emotional abuse, is laughable, and simply betrays one's self as a coddled snowflake who emerges in the real world and discovers that not everyone places their delicate feelings as their top priority. > The heavyweights that were once here - Stroustrup, James Kanze, all of them - > never used such language, ever. Linus fucking Torvalds sure did. Until he was harassed by the snowflakes into compliance. > No moderated technical group ever permits > such language because of how destructive to communities it is. Only destructive to communities of delicate snowflakes. So all these fragile flowers are freely to go way to all these moderated and curated communities. There are plenty of them around. They'll be more at home there. They don't have to worry about their delicate, pristine, victorian sensibilities getting offended by mean nasty words. > > I don't get it > I know. Let's see, someone else around here recently had a complaint about someone else constantly misquoting in their replies. Don't recall who it was… |
"Alf P. Steinbach" <alf.p.steinbach+usenet@gmail.com>: Mar 23 12:50AM +0100 On 23.03.2020 00:12, Mr Flibble wrote: > On 22/03/2020 20:29, rick.c.hodgin@gmail.com wrote: >> [snip] > Fuck. Off. Consider replying to him by e-mail, instead of posting to the group, please? I've long ago killfiled Rick but despite your occasional sausage postings, good-natured trolling attempts and habitual swearing, plus one silly impossible project (the generic compiler vaporware), I would really really hesitate to killfile you. For one, I could miss out on news on your graphics project thing. Is it possible to maybe make a "light" version of that, that doesn't depend on the generic compiler? - Alf |
Mr Flibble <flibbleREMOVETHISBIT@i42.co.uk>: Mar 22 11:12PM > My heart of love goes out to each of you. > May the Lord free you, then bind you up in His Kingdom, and keep secure you no matter what comes. > Love. Joy. Peace. Hope. In Christ alone. Fuck. Off. /Flibble -- "Snakes didn't evolve, instead talking snakes with legs changed into snakes." - Rick C. Hodgin "You won't burn in hell. But be nice anyway." – Ricky Gervais "I see Atheists are fighting and killing each other again, over who doesn't believe in any God the most. Oh, no..wait.. that never happens." – Ricky Gervais "Suppose it's all true, and you walk up to the pearly gates, and are confronted by God," Byrne asked on his show The Meaning of Life. "What will Stephen Fry say to him, her, or it?" "I'd say, bone cancer in children? What's that about?" Fry replied. "How dare you? How dare you create a world to which there is such misery that is not our fault. It's not right, it's utterly, utterly evil." "Why should I respect a capricious, mean-minded, stupid God who creates a world that is so full of injustice and pain. That's what I would say." |
You received this digest because you're subscribed to updates for this group. You can change your settings on the group membership page. To unsubscribe from this group and stop receiving emails from it send an email to comp.lang.c+++unsubscribe@googlegroups.com. |
No comments:
Post a Comment