problem with CreateThread (syntax?)

This code:

   // Start the acquisition thread
    m_hCameraThread = CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)CameraThread, (LPDWORD*)this, 0, &dwThreadId);

is giving me this error:
error C2440: 'type cast' : cannot convert from '' to 'unsigned long (__stdcall *)(void *)'

I'm not sure how to fix this?

thanks
-Paul
PMH4514Asked:
Who is Participating?

[Webinar] Streamline your web hosting managementRegister Today

x
 
jkrConnect With a Mentor Commented:
'CameraThread' needs to be declared as

DWORD WINAPI CameraThread(
  LPVOID lpParameter   // thread data
);
 
and if it is a member function of any class, be sure to make it 'static'.

0
 
AxterCommented:
Hi PMH4514,
Which argument are you getting the error on?
Can you give us the complete error message, and what are the types for the arguments you're passing in?

David Maisonave :-)
Cheers!
0
 
PMH4514Author Commented:
the complete error is:
error C2440: 'type cast' : cannot convert from '' to 'unsigned long (__stdcall *)(void *)'
        None of the functions with this name in scope match the target type

it's defined in Camera.h:

protected:
    static HANDLE m_hCameraThread;

and the eror is when I try to compile a derived class. The error occurs regardless if m_hCameraThread is static or not.  It must have something to do with the fact that I'm trying to use it in a derived class?
0
The new generation of project management tools

With monday.com’s project management tool, you can see what everyone on your team is working in a single glance. Its intuitive dashboards are customizable, so you can create systems that work for you.

 
jkrCommented:
>>The error occurs regardless if m_hCameraThread is static or not

No, I meant that the thread *function* - if a member function - needs to be static. How is 'CameraThread()' declared?
0
 
AxterCommented:
FYI:
If CameraThread is the right type, then you wouldn't need the LPTHREAD_START_ROUTINE cast.

Can you remove the LPTHREAD_START_ROUTINE, and  compile it.
You'll probably get a more informmed error.

Also please post the declaration for CameraThread.
0
 
PMH4514Author Commented:
ahh.. yup, that was it..

was like:
      virtual DWORD CameraThread(LPDWORD lpdwParam);            // thread method

now like
      static DWORD CameraThread(LPDWORD lpdwParam);            // thread method

and it compiles.

can I still implement CameraThread in the derived class w/o having defined it as virtual?
0
 
AxterCommented:
>>can I still implement CameraThread in the derived class w/o having defined it as virtual?

Yes, but it's not going to work from a base type pointer or reference.
0
 
jkrCommented:
>>can I still implement CameraThread in the derived class w/o having defined it as virtual?

You can implement one, sure. 'virtual' does not really make sense for static members, though.
0
 
AxterCommented:
In other words

Base *b = new Derived();

b->CameraThread(); //This will call the base class CameraThread function, and not the Derived::CameraThread

And if you're base class tries to pass CameraThread as an argument, it will be the base class function, and not the derived CameraThread function.
0
 
AxterCommented:
If you have derived functionallity that you want CameraThread to perform, I recommend you have CameraThread call your virtual function.

You can do this if you pass a pointer to the derived class when you create the thread.
0
 
PMH4514Author Commented:
hmm..  but a thread control method has to be static right?

basically, I have my CCamera which is a base class:

class CCamera  
{
public:
      CCamera();
      virtual ~CCamera();

      virtual DWORD StartCapture();                                    // starts capture
      static DWORD CameraThread(LPDWORD lpdwParam);            // thread method
// etc.

}

Camera.cpp does this:
DWORD CameraThread(LPDWORD lpdwParam)
{
    // Base class has no functionality
    ASSERT(FALSE);
    return ERROR_CALL_NOT_IMPLEMENTED;
}

I have two derived CCamera types, each needs to implement the thread function its own way.

in each, on my "StartCapture" method I do:

    m_hCameraThread = CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)CameraThread, (LPDWORD*)this, 0, &dwThreadId);

as well, in their respective implementations I do:

    m_hCameraThread = CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)CameraThread, (LPDWORD*)this, 0, &dwThreadId);

DWORD CameraThread(LPDWORD lpdwParam)
{
 //..
}

0
 
jkrCommented:
>>but a thread control method has to be static right?

Yes, but you can circumvent that using

DWORD SomeClass::CameraThread(LPDWORD lpdwParam)
{
   SomeClass* p = (SomeClass*) lpdwParam;

    return p->DerivedClassThreadImplementationNonStatic ();
}
0
 
PMH4514Author Commented:
so if it is static, does it have to be implemented in the base class (even if that implementation is as you just described where it forwards the call into the derived class?)
0
 
AxterCommented:
To add to above comment, make sure it's a virtual function.
Example:

class CCamera  
{
public:
     CCamera();
     virtual ~CCamera();

     virtual DWORD StartCapture();                              // starts capture
     static DWORD CameraThread(LPDWORD lpdwParam);          // thread method
     virtual DWORD CameraThread();  //Virtual function to allow derive class to have it's own methods
// etc.

}

DWORD SomeClass::CameraThread(LPDWORD lpdwParam)
{
   CCamera  * p = (CCamera  *) lpdwParam; //This really should be cast to base class type CCamera  

    return p->CameraThread(); //Now you'll get derived method via virtual function CameraThread (no parameters)
}
0
 
PMH4514Author Commented:
so my derived CameraThread, as it stands where it takes the paramater from which I can cast the class type, I can re-write it as if it were a standard class method, with direct access to its members?
0
 
jkrCommented:
>> so if it is static, does it have to be implemented in the base class

It does not have to, but that's where I'd put it.
0
 
AxterCommented:
>>so if it is static, does it have to be implemented in the base class (even if that implementation is as you just
>>described where it forwards the call into the derived class?)

I recommend that you do place the static fucntion in the base class.
You can then make your virtual function protected.

Using the method I posted above, you can just have one static CameraThread function.
And then your derived class can override the virtual CameraThread function to get specific functionallity for that class.
0
 
PMH4514Author Commented:
ok, so now I have the static CameraThread(LPDWORD lpdwParam) implemented in the base class, as Axter illustrated, where it propogates the call into the derived class CameraThread() method..

But, within my derived class, how do I create the thread? This line no longer works again

    m_hCameraThread = CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)CameraThread, (LPDWORD*)this, 0, &dwThreadId);

with the same error as in the original post.
0
 
AxterCommented:
Can you post the base class with new modifications, and the derived class?
0
 
PMH4514Author Commented:
header or .h and .cpp?
0
 
AxterCommented:
Also, I recommend you removed the (LPTHREAD_START_ROUTINE) cast.

When you post the class, please include complete delcaration so we can also see the data member types.
0
 
jkrCommented:
>>where it propogates the call into the derived class CameraThread()

Try to give that a different name...
0
 
AxterCommented:
>>header or .h and .cpp?

complete header, and just the function(s) that calls CreateThread.
0
 
PMH4514Author Commented:
here is the header for the base class:

#if !defined(AFX_CAMERA_H__DD0CED48_7873_483D_97E0_87459F8691B8__INCLUDED_)
#define AFX_CAMERA_H__DD0CED48_7873_483D_97E0_87459F8691B8__INCLUDED_

#if _MSC_VER > 1000
#pragma once
#endif // _MSC_VER > 1000


class CCamera  
{
public:
      CCamera();
      virtual ~CCamera();

      virtual DWORD StartCapture();                                    // starts capture
      virtual DWORD StopCapture();                                    // stops capture
      virtual DWORD PauseCapture();                                    // pauses capture, stores local buffer of last frame
      virtual DWORD ResumeCapture();                                    // resumes capture
      virtual DWORD Reset(BOOL a_bInitHardware = FALSE);      // resets camera
      virtual DWORD Initialize();                                          // initializes camera.
      virtual DWORD GrabImage(auto_ref_ptr<CFramePacket> pPacket);      // populates pPacket->Buffer with image data.
      virtual DWORD StartRecord();
      virtual DWORD StopRecord();
      virtual DWORD PauseRecord();
      virtual DWORD CaptureFrame();                                    // informs CPacketFilter to capture next packet.
      static DWORD CameraThread(LPDWORD lpdwParam);            // thread method
      virtual DWORD CameraThread();                                    // to allow derived classes their own implementations
protected:
      BYTE* m_pPauseBuffer;                                                // copy of last frame before pause.
                                                                                    // is pushed to GrabFrame(BYTE* pBuffer) if capture paused.
    HANDLE m_hCameraThread;
      CConfigManager* m_pConfigManager;      
};

#endif // !defined(AFX_CAMERA_H__DD0CED48_7873_483D_97E0_87459F8691B8__INCLUDED_)


-------------------------------

and the header for the derived class:

class CCameraImaq : public CCamera  
{
public:
      CCameraImaq();
      virtual ~CCameraImaq();
    DWORD SetAcquisitionWindow();

      DWORD StartCapture();                                    // starts capture
      DWORD StopCapture();                                    // stops capture
      DWORD PauseCapture();                                    // pauses capture, stores local buffer of last frame
      DWORD StartRecord();                                    // starts video capture
      DWORD StopRecord();                                          // stops video capture
      DWORD PauseRecord();                                    // pauses video capture
      DWORD CaptureFrame();                                    // informs CPacketFilter to capture next packet.
      DWORD Reset(BOOL a_bInitHardware = FALSE);      // resets camera
      DWORD Initialize();                                          // initializes camera.
      DWORD GrabImage(auto_ref_ptr<CFramePacket> pPacket);      // populates pPacket->Buffer with image data.
      DWORD CameraThread();                                    


private:
    SESSION_ID   m_SessionID;
    INTERFACE_ID m_InterfaceID;

    long        m_AcqWinWidth;
    long        m_AcqWinHeight;
    long        m_CanvasWidth;            // 640(512) width of the display area
    long        m_CanvasHeight;            // 512(384) height of the display area
    long        m_CanvasTop;            // top of the display area
    long        m_CanvasLeft;            // left of the display area

      int m_iSnapDelay;

      DWORD InitializeCard();
};


and then I won't post the implementation of the derived class, it's long, the line in question is:

   m_hCameraThread = CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)CameraThread, (LPDWORD*)this, 0, &dwThreadId);


0
 
PMH4514Author Commented:
(complete method for create thread)

DWORD CCameraImaq::StartCapture()
{
    DWORD dwThreadId;

    // Start the acquisition thread
    m_hCameraThread = CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)CameraThread, (LPDWORD*)this, 0, &dwThreadId);

    if (m_hCameraThread == NULL)
        return GetLastError();

         return ERROR_SUCCESS;
}
0
 
AxterCommented:
>>and then I won't post the implementation of the derived class, it's long, the line in question is:
>>   m_hCameraThread = CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)CameraThread, (LPDWORD*)this,
>>0, &dwThreadId);

Try the following:
m_hCameraThread = CreateThread(NULL, 0, CCamera::CameraThread, (LPDWORD*)this, 0, &dwThreadId);

Put the CCamera:: prefix on the static function argument, and remove the casting.
0
 
PMH4514Author Commented:
nope:

error C2664: 'CreateThread' : cannot convert parameter 3 from 'unsigned long (void)' to 'unsigned long (__stdcall *)(void *)'
        None of the functions with this name in scope match the target type


(I'll be gone now for a few hours.. carry on :)

0
 
AxterCommented:
This should fix above error:
Change your static declaration to the following:

static DWORD WINAPI CameraThread(LPVOID  lpdwParam);          // thread method
0
 
PMH4514Author Commented:
yes Axter that fixed that..

I must have skrewed something, up now I'm getting a bunch of errors like this:

CameraImaq.obj : error LNK2005: "unsigned long __cdecl CameraThread(unsigned long *)" (?CameraThread@@YAKPAK@Z) already defined in Camera.obj
CameraMacro.obj : error LNK2005: "unsigned long __cdecl CameraThread(unsigned long *)" (?CameraThread@@YAKPAK@Z) already defined in Camera.obj
Camera.obj : error LNK2001: unresolved external symbol "public: virtual unsigned long __thiscall CCamera::CameraThread(void)" (?CameraThread@CCamera@@UAEKXZ)
CameraMacro.obj : error LNK2001: unresolved external symbol "public: virtual unsigned long __thiscall CCamera::CameraThread(void)" (?CameraThread@CCamera@@UAEKXZ)
Camera.obj : error LNK2001: unresolved external symbol "public: virtual unsigned long __thiscall CCamera::PauseRecord(void)" (?PauseRecord@CCamera@@UAEKXZ)

those symbols are all defined in their respective classes
0
 
PMH4514Author Commented:
oh.. duh, disregard that last post
0
 
PMH4514Author Commented:
axter: I missed this comment from you ealier:

>If CameraThread is the right type, then you wouldn't need the LPTHREAD_START_ROUTINE cast.

could you please ellaborate?

thanks
0
 
AxterConnect With a Mentor Commented:
>>could you please ellaborate?

Without the cast, the compiler can more accurately compare the types, to determine if you have the right argument type.

If the CameraThread function is of the right type, then you don't need to cast it.

You should avoid casting as much possible, and you should only cast when you're absolutely sure that it's right.
When you cast, you're telling the compiler that you know what you're doing, and to ignore what the compiler thinks is a potential error.

There are times when you really do need to cast:
Example:

DWORD SomeClass::CameraThread(LPDWORD lpdwParam)
{
   CCamera  * p = (CCamera  *) lpdwParam; //This is a good place for casting

The above line is a good reason for casting, although most C++ programmers would recommend that a C++ cast be used instead of a C-Cast.
0
 
PMH4514Author Commented:
ok I see.

I am still having a compile problem with regard to the CreateThread

again, my base class defines:

      static DWORD WINAPI CameraThread(LPVOID  lpdwParam);          // thread method
      virtual DWORD CameraThread();                                    // to allow derived classes their own implementations


my derived class implements:

DWORD CCameraImaq::CameraThread()
{
 //
}

my derived class trys to create the thread:
      m_hCameraThread = CreateThread(NULL, 0, CCamera::CameraThread, (LPDWORD*)this, 0, &dwThreadId);
and I get:

CameraImaq.obj : error LNK2001: unresolved external symbol "public: static unsigned long __stdcall CCamera::CameraThread(void *)" (?CameraThread@CCamera@@SGKPAX@Z)
Debug/vivascope.exe : fatal error LNK1120: 1 unresolved externals

if I comment that line out (CreateThread) it compiles fine.
0
 
PMH4514Author Commented:
>>although most C++ programmers would recommend that a C++ cast be used instead of a C-Cast.

what's the difference? (syntactically and otherwise)
0
 
AxterCommented:
The error you just posted is referring to the static function, and not the virtual function.

Please post the implementation for your static CameraThread.

Should be modified to match your class declaration.

DWORD WINAPI CCamera::CameraThread(LPVOID lpdwParam)
{
   CCamera  * p = (CCamera  *) lpdwParam; //This really should be cast to base class type CCamera  

    return p->CameraThread(); //Now you'll get derived method via virtual function CameraThread (no parameters)
}

0
 
AxterCommented:
>>what's the difference? (syntactically and otherwise)

C++ has four different cast.
dynamic_cast
static_cast
const_cast
reinterpret_cast

It's suppose to be safer to use these cast because it narrows the type of cast you can do, and it suppose to tell anyone who's reading your code more information as to why you're casting.
I don't noramlly use these cast myself, since I'm not a fan of them, so I wouldn't be a good expert to give you much more details on their usage.
0
 
PMH4514Author Commented:
oh yeah, duh, forgot to change the interface to match the WINAPI declaration, now it works

thanks
-Paul
0
 
itsmeandnobodyelseCommented:
>> so if it is static, does it have to be implemented in the base class    

>> static DWORD WINAPI CameraThread(LPVOID  lpdwParam);          // thread method
>> virtual DWORD CameraThread();      // to allow derived classes their own implementations

Instead of using virtual function it would have been much simpler using static functions of the derived classes. It seems to me that the recommendation to put the thread start function to the baseclass, given both by jkr and Axter,  was based on a misunderstanding. If you have two classes each of them needs an own implementation of  it's own thread, you easily could use the static member defined in the derived class.

Regards, Alex
0
 
PMH4514Author Commented:
>>It seems to me that the recommendation to put the thread start function to the baseclass, given both by jkr and Axter,  was based on a misunderstanding.

what misunderstanding was that? (just to clarify for me the differences)

Basically my system is connected physically to 2 different types of camera hardware. Only one will be capturing and presenting data to the screen at a given time (for now at least, there's talk of dual monitors, but that's off a ways). I've defined the base class CCamera so that they each implement the same interface, but each needs its own implementation of the thread, of course, because physical hardware differences require different code to interface with the hardware. From an interface standpoint, I want all "cameras" to be the same so that the rest of my system doesn't care where the image data came from.

If putting the thread start function in the base class was a misunderstood suggestion, could you describe the scenario in which doing so is appropriate?

thanks!
-Paul
0
 
itsmeandnobodyelseCommented:
>>>> what misunderstanding was that?

I suppose there was a misunderstanding because you defined the initial thread start function as virtual.

But virtual functions only make sense if you are using baseclass pointers of different derived classes. If you already have a derived object, e. g. the this pointer of a member function of the derived class, or a pointer to the derived class, there is no need to use virtuality.

However, you should know that static member functions don't have a 'this' object but are some kind of global functions - that's the reason you can pass them via function pointer to a thread. So, the suggestion of jkr to pass the this pointer as an argument to the thread, make a cast to the pointer in the thread start function and call a non-static member function, is very recommendable independent of any virtuality issue.

Regards, Alex
0
 
PMH4514Author Commented:
interesting.. I guess I thought if you had the interface for a method defined in a base class that you intended to override in a derived class (wouldn't you always?) that you had to declare it as virtual.
0
 
AxterCommented:
>>interesting.. I guess I thought if you had the interface for a method defined in a base class that you intended to
>>override in a derived class (wouldn't you always?) that you had to declare it as virtual.

If it's possible that your base class, or any upper level class is going to create the thread with a derived class object, then you do want to use a virtual class.

It it's possible that you want to allow a sub class or over ride the thread logic, then you want to use a virtual class.

IMHO, it's safer to just use the virtual class by default, so that you don't run into problem later if the code gets changed.

It also makes more sense to me to just create one static function.
Otherwise you would have to create a static function for each derived class.

From an OO point of view, I think it makes much more sense to use the static function / virtual function combination.
0
 
itsmeandnobodyelseCommented:
Indeed,  "overriding" a baseclass function makes only sense if the baseclass function as defined as virtual.

*But*, the virtuality only comes to play if you call the virtual function using a baseclass pointer of a derived object.

 class A
 {
 public:
       virtual void f();      
 };

 class B : public A
 {
 public:
       virtual void f() { cout << "B::f" << endl; }      
 };

 class C : public A
 {
 public:
       virtual void f() { cout << "C::f" << endl; }      
       void g()          { f(); }
 };


  ...
  C c;
  c.f();   // calls C::f

  B* pB = new B;
  pB->f();  // calls B::f

  c.g();   // call C::f

  A* pA = &c;
  pA->f();    // calls C::f

  pA = pB;

  pA->f();   // calls B::f


You see that only the last two samples are using virtuality but actually could have used the derived objects also. Using baseclass pointers and virtuality normally is used if you have containers that could contain pointers of different derived objects but where your program doesn't know at runtime which derived object actually was stored.

Regards, Alex

 
 
0
 
itsmeandnobodyelseCommented:
>>>> Otherwise you would have to create a static function for each derived class.

But the questioner says that he needs two different implementations of the thread functionality. And I couldn't see any baseclass pointer involved til now.

Regards, Alex

0
 
AxterCommented:
>>*But*, the virtuality only comes to play if you call the virtual function using a baseclass pointer of a derived object.

Not true at all.

See following code:
class A
{
public:
      virtual void f() { cout << "A::f" << endl; }  
      void SomeBaseFunction(){ f(); }
};

class B : public A
{
public:
      virtual void f() { cout << "B::f" << endl; }      
};

class C : public A
{
public:
      virtual void f() { cout << "C::f" << endl; }      
      void g()          { f(); }
};



int main(int argc, char* argv[])
{
      C c;
      c.SomeBaseFunction();// Indirect call to C::f
      B b;
      b.SomeBaseFunction();// Indirect call to B::f
0
 
PMH4514Author Commented:
this is what polymorphism is all about right?
0
 
AxterCommented:
>>But the questioner says that he needs two different implementations of the thread functionality. And I couldn't see
>>any baseclass pointer involved til now.

You don't need a base class pointer.

If you look at my version of your code, I don't use any pointers at all.
But if you want the base class to perform some logic in the correct way, based on possible derived class overriden function, you need the function to be virtual.

So to associated this with CCamera class, if you ever want CCamera class to either call CameraThread derived function directly, or if you want to pass the job of creating the thread to the CCamera class, then you need to have a virtual function so that the correct derived class function gets called.
And the above requirement has nothing to do with using a pointer.
0
 
AxterCommented:
>>this is what polymorphism is all about right?

That's right.
Using the same code signature to get different results, depending on the object.
0
 
PMH4514Author Commented:
so if class A implements a method Foo(), and class B is derived from A, and B wants to have its own implementation of Foo() that starts by executing A::Foo(), how does one do that? ie. execute the base class implementation, and then continue on with additional instructions?

0
 
itsmeandnobodyelseCommented:
Axter has it right. If you want to use a non-virtual baseclass function to create the threads, you need the static/virtual combination from above. That is cause the baseclass member function only should pass a static member function of it's own class and virtuality will call the overridden functions later.  

If you have different functions to create the threads, where the functions already know what derived type is involved,  you also could use static functions of the derived classes as there is no need for virtuality then.

Regards, Alex


 
0
 
AxterCommented:
>>its own implementation of Foo() that starts by executing A::Foo(), how does one do that?

You call  A::Foo() at the start of derived function.

class B : public A
{
public:
     virtual void f()
     {
        A::Foo(); //Call base class first
        cout << "B::f" << endl;
     }      
};
0
 
PMH4514Author Commented:
excellent.. easy enough.
0
 
PMH4514Author Commented:
what about constructors/destructors -can common functionality be placed in the base class constructor/destructor methods? if so how do I call up to that within the derived constructor?
0
 
itsmeandnobodyelseCommented:
>>>> that starts by executing A::Foo(),

A::Foo() always would call A::Foo() and never B::Foo();

As already said, you need either a baseclass pointer or a baseclass function (non-virtual or not overridden) to get polymorphism. In both cases the decision was at runtime what function was called depending on the object.

So it is

   A* pA = getSomePointerOfADerivedOrBaseClass();
   pA->Foo();     // here B::Foo or A::Foo may be called depending on the return above


   void A::NonVirtualFoo()
   {
        Foo();    // here B::Foo or A::Foo may be called depending on 'this' object

   };



0
 
itsmeandnobodyelseConnect With a Mentor Commented:
>>> what about constructors/destructors -can common functionality be placed in the base class
>>> constructor/destructor methods?

Yes, but you can't call virtual functions in the baseclass constructor/destructor. The reason for this is, that all constructors/destructors are called at creation/destruction and not as it is with normal function calls only *one* function. So, at creation of the baseclass in baseclass constructor, the derived object isn't yet available cause the constructor of the derived class gets called later. The destructors also were called in opposite order, the baseclass destructor was last.
0
 
AxterCommented:
>>> what about constructors/destructors -can common functionality be placed in the base class
>>> constructor/destructor methods?

itsmeandnobodyelse is absolutely right.

You can't call derived (virtual) functions from a constructor or destructor because at that point in the code the derived object does not exist.

0
 
PMH4514Author Commented:
I see.

how 'about if a function is defined as virtual, has an implementation in the base class, and no implementation in the derived class, and I call pDerived->FunctionOnlyImplementedInBaseClass() - will it get executed? Do I have to implement it still within the base class and merely pass the call up the chain?
0
 
AxterCommented:
>>how 'about if a function is defined as virtual, has an implementation in the base class, and no implementation in the
>>derived class, and I call pDerived->FunctionOnlyImplementedInBaseClass() - will it get executed?

It will execute the base class function.
That's the beautiy of a regular virtual function.
You can let the base class function do the work, or your derived class can optionally have it's own overritten function do the job.
0
 
PMH4514Author Commented:
ooooohh..

now I really get what virtual is for.. nice!
0
 
PMH4514Author Commented:
you guys make it so hard for me to determine points :o)
0
 
itsmeandnobodyelseCommented:
>>> you guys make it so hard for me to determine points :o)

Increase to 500 and split points ;-)  Isn't it simple?


0
 
PMH4514Author Commented:
sounds good to me! I learned alot from this discussion
0
All Courses

From novice to tech pro — start learning today.